文本分類是自然語言處理中一個非常經典的任務,可用的模型非常多,相關的開源代碼也非常多了。這篇博客用一個CNN模型,對新聞文本進行分類。
全部代碼有4個模塊:1、數據處理模塊(命名為:cnews_loader.py) ;2、模型搭建模塊(命名為cnn_model.py);3、模型運行模塊(命名為run_cnn.py);4、模型預測模塊(命名為predict.py)。
GitHub地址:https://github.com/DengYangyong/Chinese_Text_Classification/tree/master/Text_Classification_On_CNN
下面分別來看各模塊的代碼。
一、數據預處理
首先做數據預處理,從原始的新聞語料得到字典,將所有新聞文本轉化為數字,生成批量數據。
這段代碼首先考慮了在python2環境運行時的編碼問題,在打開文檔時,把文本的格式編碼成 UTF-8格式,在處理文本時解碼為Unicode格式。
接下來讀取文本文件,把每一篇新聞處理成字列表: ['黃', '蜂', 'v', 's', '湖', '人', '首', '發', ':', '科', '比',...]。因為這是CharCNN,所以不需要進行分詞,也不用去停用詞(標點符號),比較省事。這里沒法使用預訓練的word2vec,因為word2vec是用當前的詞預測上下文詞語,或者上下文詞語預測當前詞語,因此必須是黃峰、首發這種詞語,而不能是單個的字。
然后統計詞頻,並取出頻率最高的前5000個字,做成一個字典。利用這個字典,把每一篇新聞都轉化為數字索引,並且把每一篇新聞的長度設定為600字,字數多余600字則截斷,字數少於600字則補零。將標簽轉化為one-hot編碼。
最后是生成批量數據,每個批量64個樣本,在訓練、驗證和測試模型時,都使用小批量數據,防止內存溢出。
#coding: utf-8 import sys from collections import Counter import numpy as np import tensorflow.contrib.keras as kr if sys.version_info[0] > 2: is_py3 = True else: reload(sys) sys.setdefaultencoding("utf-8") is_py3 = False # 判斷軟件的版本,如果版本為3.6.5,那么sys.version_info的輸出為:sys.version_info(major=3, minor=6, micro=5)。 """如果在python2下面使用python3訓練的模型,可考慮調用此函數轉化一下字符編碼""" def native_word(word, encoding='utf-8'): if not is_py3: return word.encode(encoding) else: return word """is_py3函數當版本為3時返回True,否則返回False。if not 后面的值為False則將“utf-8”編碼轉換為'unicode'.""" def native_content(content): if not is_py3: return content.decode('utf-8') else: return content """ 常用文件操作,可在python2和python3間切換.""" def open_file(filename, mode='r'): if is_py3: return open(filename, mode, encoding='utf-8', errors='ignore') else: return open(filename, mode) """ 讀取文件數據""" def read_file(filename): contents, labels = [], [] with open_file(filename) as f: for line in f: try: label, content = line.strip().split('\t') if content: contents.append(list(native_content(content))) labels.append(native_content(label)) except: pass return contents, labels # line.strip().split('\t')的輸出為兩個元素的列表:['體育', '黃蜂vs湖人首發:科比帶傷戰保羅 加索爾救贖之戰 新浪體育訊...']。 # 注意這個list()函數,把一段文字轉化為了列表,元素為每個字和符號:['黃', '蜂', 'v', 's', '湖', '人', '首', '發', ':', '科', '比',...] # contents的元素為每段新聞轉化成的列表:[['黃', '蜂', 'v', 's', '湖', '人', '首', '發', ':', '科', '比',...],[],...] # labels為['體育', '體育',...] """根據訓練集構建詞匯表,存儲""" def build_vocab(train_dir, vocab_dir, vocab_size=5000): data_train, _ = read_file(train_dir) all_data = [] for content in data_train: all_data.extend(content) counter = Counter(all_data) count_pairs = counter.most_common(vocab_size - 1) words, _ = list(zip(*count_pairs)) words = ['<PAD>'] + list(words) open_file(vocab_dir, mode='w').write('\n'.join(words) + '\n') '''讀取詞匯表''' def read_vocab(vocab_dir): with open_file(vocab_dir) as fp: words = [native_content(_.strip()) for _ in fp.readlines()] word_to_id = dict(zip(words, range(len(words)))) return words, word_to_id # readlines()讀取所有行然后把它們作為一個字符串列表返回:['頭\n', '天\n', ...]。strip()函數去掉"\n"。 # words: ['<PAD>', ',', '的', '。', '一', '是', '在', '0', '有',...] # word_to_id:{'<PAD>': 0, ',': 1, '的': 2, '。': 3, '一': 4, '是': 5,..},每個類別對應的value值為其索引ID """讀取分類目錄""" def read_category(): categories = ['體育', '財經', '房產', '家居', '教育', '科技', '時尚', '時政', '游戲', '娛樂'] categories = [native_content(x) for x in categories] cat_to_id = dict(zip(categories, range(len(categories)))) return categories, cat_to_id # cat_to_id的輸出為:{'體育': 0, '財經': 1, '房產': 2, '家居': 3,...},每個類別對應的value值為其索引ID. """ 將id表示的內容轉換為文字 """ def to_words(content, words): return ''.join(words[x] for x in content) """ 將文件轉換為id表示,進行pad """ def process_file(filename, word_to_id, cat_to_id, max_length=600): contents, labels = read_file(filename) data_id, label_id = [], [] #contents的形式為:[['黃', '蜂', 'v', 's', '湖', '人',...],[],[],...],每一個元素是一個列表,該列表的元素是每段新聞的字和符號。 #labels的形式為:['體育', '體育', '體育', '體育', '體育', ...] for i in range(len(contents)): data_id.append([word_to_id[x] for x in contents[i] if x in word_to_id]) label_id.append(cat_to_id[labels[i]]) x_pad = kr.preprocessing.sequence.pad_sequences(data_id, max_length) y_pad = kr.utils.to_categorical(label_id, num_classes=len(cat_to_id)) return x_pad, y_pad # word_to_id是一個字典:{'<PAD>': 0, ',': 1, '的': 2, '。': 3, '一': 4, '是': 5,...} # 對於每一段新聞轉化的字列表,把每個字在字典中對應的索引找到: # data_id: 將[['黃', '蜂', 'v', 's', '湖', '人',...],[],[],...] 轉化為 [[387, 1197, 2173, 215, 110, 264,...],[],[],...]的形式 # label_id : ['體育', '體育', '體育', '體育', '體育', ...] 轉化為[0, 0, 0, 0, 0, ...] # data_id的行數為50000,即為新聞的條數,每個元素為由每段新聞的字的數字索引構成的列表; # data_id長這樣:[[387, 1197, 2173, 215, 110, 264,...],[],[],...] # 由於每段新聞的字數不一樣,因此每個元素(列表)的長度不一樣,可能大於600,也可能小於600,需要統一長度為600。 # 使用keras提供的pad_sequences來將文本pad為固定長度,x_pad的形狀為(50000,600). # label_id是形如[0, 0, 0, 0, 0, ...]的整形數組,cat_to_id是形如{'體育': 0, '財經': 1, '房產': 2, '家居': 3,...}的字典 # to_categorical是對標簽進行one-hot編碼,num-classes是類別數10,y_pad的維度是(50000,10) """生成批次數據""" def batch_iter(x, y, batch_size=64): data_len = len(x) num_batch = int((data_len - 1) / batch_size) + 1 indices = np.random.permutation(np.arange(data_len)) x_shuffle = x[indices] y_shuffle = y[indices] # 樣本長度為50000 # int()可以將其他類型轉化為整型,也可以用於向下取整,這里為782. # indices元素的范圍是0-49999,形如[256,189,2,...]的擁有50000個元素的列表 # 用indices對樣本和標簽按照行進行重新洗牌,接着上面的例子,把第256行(從0開始計)放在第0行,第189行放在第1行. for i in range(num_batch): start_id = i * batch_size end_id = min((i + 1) * batch_size, data_len) yield x_shuffle[start_id:end_id], y_shuffle[start_id:end_id] # i=780時,end_id=781*64=49984; # 當i=781時,end_id=50000,因為782*64=50048>50000,所以最后一批取[49984:50000] # yield是生成一個迭代器,用for循環來不斷生成下一個批量。 # 為了防止內存溢出,每次只取64個,內存占用少。
二、搭建模型
這一個模塊是搭建TextCNN模型和配置相關的參數。
基本就是按照以上這張圖的模型結構來搭建的,首先是一個embedding層,將字轉化為64維的向量。然后進行一維卷積,提取出256個特征后,進行最大池化。最后接全連接層,在全連接進行dropout。相比圖像識別中的CNN模型,這個模型比較簡單。
# coding: utf-8 import tensorflow as tf """ CNN配置參數 """ class TCNNConfig(object): embedding_dim = 64 seq_length = 600 num_classes = 10 num_filters = 256 kernel_size = 5 # 輸入層的維度是(600,64,1) # 卷積核數目是256,也就是提取的特征數量是256種,決定了卷積層的通道數為256 # 卷積核的維度是(5,64) # 卷積核尺寸為5,也就是一次卷多少個詞,這里卷5個詞,那么是5-gram。 # 卷積層的維度是(600-5+1,1,256),如果Stride=1, n-gram=5。256是由卷積核的個數決定的。 # 卷積層的通道數等於卷積核的個數,卷積核的通道數等於輸入層的通道數。 vocab_size = 5000 hidden_dim = 128 dropout_keep_prob = 0.5 learning_rate = 1e-3 batch_size = 64 num_epochs = 10 print_per_batch = 100 save_per_batch = 10 # 每100批輸出一次結果。 # 每10批存入tensorboard。 """文本分類,CNN模型""" class TextCNN(object): def __init__(self, config): self.config = config # None是bitch_size,input_x是(64,600)的維度,input_y的維度是(64,10) self.input_x = tf.placeholder(tf.int32, [None, self.config.seq_length], name='input_x') self.input_y = tf.placeholder(tf.float32, [None, self.config.num_classes], name='input_y') self.keep_prob = tf.placeholder(tf.float32, name='keep_prob') self.cnn() def cnn(self): with tf.device('/gpu:0'):
embedding = tf.get_variable('embedding', [self.config.vocab_size, self.config.embedding_dim]) embedding_inputs = tf.nn.embedding_lookup(embedding, self.input_x) # 指定在第1塊gpu上運行,如果指定是cpu則('/cpu:0') # 獲取已經存在的變量,不存在則創建並隨機初始化。這里的詞向量是隨機初始化的,embedding的維度是(5000,64) # embedding_inputs.shape=(64,600,64) with tf.name_scope("cnn"): conv = tf.layers.conv1d(embedding_inputs, self.config.num_filters, self.config.kernel_size, name='conv') gmp = tf.reduce_max(conv, reduction_indices=[1], name='gmp') # 使用一維卷積核進行卷積,因為卷積核的第二維與詞向量維度相同,只能沿着行向下滑動。 # 輸入樣本維度是(600,64),經過(5,64)的卷積核卷積后得到(596,1)的向量(600-5+1=596),默認滑動為1步。 # 由於有256個過濾器,於是得到256個(596,1)的向量。 # 結果顯示為(None,596,256) # 用最大池化方法,按行求最大值,conv.shape=[Dimension(None), Dimension(596), Dimension(256)],留下了第1和第3維。 # 取每個向量(596,1)中的最大值,然后就得到了256個最大值, # gmp.shape=(64,256) with tf.name_scope("score"): fc = tf.layers.dense(gmp, self.config.hidden_dim, name='fc1') fc = tf.contrib.layers.dropout(fc, self.keep_prob) fc = tf.nn.relu(fc) # 全連接層,后面接dropout以及relu激活 # 神經元的個數為128個,gmp為(64,256),經過這一層得到fc的維度是(64,128) self.logits = tf.layers.dense(fc, self.config.num_classes, name='fc2') self.y_pred_cls = tf.argmax(tf.nn.softmax(self.logits), 1) # softmax得到的輸出為[Dimension(None), Dimension(10)],是10個類別的概率 # 然后再從中選出最大的那個值的下標,如[9,1,3...] # 最后得到的是(64,1)的列向量,即64個樣本對應的類別。 with tf.name_scope("optimize"): cross_entropy = tf.nn.softmax_cross_entropy_with_logits_v2(logits=self.logits, labels=self.input_y) self.loss = tf.reduce_mean(cross_entropy) self.optim = tf.train.AdamOptimizer(learning_rate=self.config.learning_rate).minimize(self.loss) # tf.reduce_mean(input_tensor,axis)用於求平均值,這里是求64個樣本的交叉熵損失的均值。 with tf.name_scope("accuracy"): correct_pred = tf.equal(tf.argmax(self.input_y, 1), self.y_pred_cls) self.acc = tf.reduce_mean(tf.cast(correct_pred, tf.float32)) # 准確率的計算,tf.equal對內部兩個向量的每個元素進行對比,返回[True,False,True,...]這樣的向量 # 也就是對預測類別和標簽進行對比,self.y_pred_cls形如[9,0,2,3,...] # tf.cast函數將布爾類型轉化為浮點型,True轉為1.,False轉化為0.,返回[1,0,1,...] # 然后對[1,0,1,...]這樣的向量求均值,恰好就是1的個數除以所有的樣本,恰好是准確率。
三、訓練、驗證和測試模型
以下的代碼分別定義了用於訓練、驗證和測試的函數,需要注意的是在驗證和測試時是不用進行dropout的,也就是保留比例設定為1。並且用早停來防止過擬合。
#!/usr/bin/python # -*- coding: utf-8 -*- #哪怕程序在2.7的版本運行,也可以用print()這種語法來打印。 from __future__ import print_function import os import sys import time from datetime import timedelta import numpy as np import tensorflow as tf from sklearn import metrics from cnn_model import TCNNConfig, TextCNN from cnews_loader import read_vocab, read_category, batch_iter, process_file, build_vocab # 數據處理模塊為cnews_loader # 模型搭建模塊為cnn_model base_dir = 'data/cnews' train_dir = os.path.join(base_dir, 'cnews.train.txt') test_dir = os.path.join(base_dir, 'cnews.test.txt') val_dir = os.path.join(base_dir, 'cnews.val.txt') vocab_dir = os.path.join(base_dir, 'cnews.vocab.txt') save_dir = 'checkpoints/textcnn' save_path = os.path.join(save_dir, 'best_validation') #這里說是保存路徑,其實這個“best_validation”是保存的文件的名字的開頭,比如保存的一個文件是“best_validation.index” def get_time_dif(start_time): """獲取已使用時間""" end_time = time.time() time_dif = end_time - start_time return timedelta(seconds=int(round(time_dif))) # round函數是對浮點數四舍五入為int,注意版本3中round(0.5)=0,round(3.567,2)=3.57。 # timedelta是用於對間隔進行規范化輸出,間隔10秒的輸出為:00:00:10 def feed_data(x_batch, y_batch, keep_prob): feed_dict = { model.input_x: x_batch, model.input_y: y_batch, model.keep_prob: keep_prob } return feed_dict """ 評估在某一數據上的准確率和損失 """ def evaluate(sess, x_, y_): data_len = len(x_) batch_eval = batch_iter(x_, y_, 128) total_loss = 0.0 total_acc = 0.0 for x_batch, y_batch in batch_eval: batch_len = len(x_batch) feed_dict = feed_data(x_batch, y_batch, 1.0) loss, acc = sess.run([model.loss, model.acc], feed_dict=feed_dict) total_loss += loss * batch_len total_acc += acc * batch_len return total_loss / data_len, total_acc / data_len # 1.0是dropout值,在測試和驗證時不需要舍棄 # 把feed_dict的數據傳入去計算model.loss,是求出了128個樣本的平均交叉熵損失 # 把平均交叉熵和平均准確率分別乘以128個樣本得到總數,不斷累加得到10000個樣本的總數。 # 求出10000個樣本的平均交叉熵,和平均准確率。 def train(): print("Configuring TensorBoard and Saver...") tensorboard_dir = 'tensorboard/textcnn' if not os.path.exists(tensorboard_dir): os.makedirs(tensorboard_dir) tf.summary.scalar("loss", model.loss) tf.summary.scalar("accuracy", model.acc) # 用到 tf.summary 中的方法保存日志數據,用於tensorboard可視化操作。 # 用 tf.summary.scalar 保存標量,一般用來保存loss,accuary,學習率等數據 merged_summary = tf.summary.merge_all() writer = tf.summary.FileWriter(tensorboard_dir) # 使用 tf.summaries.merge_all() 對所有的匯總操作進行合並 # 將數據寫入本地磁盤: tf.summary.FileWriter saver = tf.train.Saver() if not os.path.exists(save_dir): os.makedirs(save_dir) print("Loading training and validation data...") start_time = time.time() x_train, y_train = process_file(train_dir, word_to_id, cat_to_id, config.seq_length) x_val, y_val = process_file(val_dir, word_to_id, cat_to_id, config.seq_length) time_dif = get_time_dif(start_time) print("Time usage:", time_dif) session = tf.Session() session.run(tf.global_variables_initializer()) writer.add_graph(session.graph) print('Training and evaluating...') start_time = time.time() total_batch = 0 best_acc_val = 0.0 last_improved = 0 require_improvement = 1000 # 如果超過1000輪未提升,提前結束訓練,防止過擬合。 flag = False for epoch in range(config.num_epochs): print('Epoch:', epoch + 1) batch_train = batch_iter(x_train, y_train, config.batch_size) for x_batch, y_batch in batch_train: feed_dict = feed_data(x_batch, y_batch, config.dropout_keep_prob) if total_batch % config.save_per_batch == 0: s = session.run(merged_summary, feed_dict=feed_dict) writer.add_summary(s, total_batch) if total_batch % config.print_per_batch == 0: feed_dict[model.keep_prob] = 1.0 loss_train, acc_train = session.run([model.loss, model.acc], feed_dict=feed_dict) loss_val, acc_val = evaluate(session, x_val, y_val) # todo if acc_val > best_acc_val: # 保存最好結果 best_acc_val = acc_val last_improved = total_batch saver.save(sess=session, save_path=save_path) improved_str = '*' else: improved_str = '' time_dif = get_time_dif(start_time) msg = 'Iter: {0:>6}, Train Loss: {1:>6.2}, Train Acc: {2:>7.2%},' \ + ' Val Loss: {3:>6.2}, Val Acc: {4:>7.2%}, Time: {5} {6}' print(msg.format(total_batch, loss_train, acc_train, loss_val, acc_val, time_dif, improved_str)) session.run(model.optim, feed_dict=feed_dict) # 運行優化 total_batch += 1 if total_batch - last_improved > require_improvement: # 驗證集正確率長期不提升,提前結束訓練 print("No optimization for a long time, auto-stopping...") flag = True break if flag: break def test(): print("Loading test data...") start_time = time.time() x_test, y_test = process_file(test_dir, word_to_id, cat_to_id, config.seq_length) session = tf.Session() session.run(tf.global_variables_initializer()) saver = tf.train.Saver() saver.restore(sess=session, save_path=save_path) # 在保存和恢復模型時都需要首先運行這一行:tf.train.Saver(),而不是只有保存時需要。 print('Testing...') loss_test, acc_test = evaluate(session, x_test, y_test) # 返回了10000個總測試樣本的平均交叉熵損失和平均准率。 msg = 'Test Loss: {0:>6.2}, Test Acc: {1:>7.2%}' print(msg.format(loss_test, acc_test)) batch_size = 128 data_len = len(x_test) num_batch = int((data_len - 1) / batch_size) + 1 y_test_cls = np.argmax(y_test, 1) y_pred_cls = np.zeros(shape=len(x_test), dtype=np.int32) for i in range(num_batch): start_id = i * batch_size end_id = min((i + 1) * batch_size, data_len) feed_dict = { model.input_x: x_test[start_id:end_id], model.keep_prob: 1.0 } y_pred_cls[start_id:end_id] = session.run(model.y_pred_cls, feed_dict=feed_dict) # 測試的時候不需要dropout神經元。 print("Precision, Recall and F1-Score...") print(metrics.classification_report(y_test_cls, y_pred_cls, target_names=categories)) # 可以得到准確率 、召回率和F1_score # 混淆矩陣 print("Confusion Matrix...") cm = metrics.confusion_matrix(y_test_cls, y_pred_cls) print(cm) time_dif = get_time_dif(start_time) print("Time usage:", time_dif) if __name__ == '__main__': config = TCNNConfig() if not os.path.exists(vocab_dir): build_vocab(train_dir, vocab_dir, config.vocab_size) categories, cat_to_id = read_category() words, word_to_id = read_vocab(vocab_dir) # 如果不存在詞匯表,重建,值為False時進行重建。 # 字典中有5000個字。 # 返回categories:['體育', '財經', '房產', '家居', '教育', '科技', '時尚', '時政', '游戲', '娛樂'] # 以及cat-to-id:{'體育': 0, '財經': 1, '房產': 2 , '家居': 3, '教育': 4, '科技': 5, '時尚': 6, '時政': 7, '游戲': 8, '娛樂': 9} # 輸出word:['<PAD>', ',', '的', '。', '一', '是', '在', '0', '有', '不', '了', '中', '1', '人', '大', '、', '國', '', '2', ...] # 輸出word_to_id:{'<PAD>': 0, ',': 1, '的': 2, '。': 3, '一': 4, '是': 5,...},里面還包含了一些字號、逗號和數字作為鍵值。 config.vocab_size = len(words) model = TextCNN(config) option='train'
# 選則 train 則為訓練模式,輸入 test 則為測試模式 if option == 'train': train() else: test()
在第4000次迭代時停止了,驗證精度為95.26%,測試精度為96.41%。並且可以看到每個類別的准確率、召回率和F1值、混淆矩陣。
Epoch: 6 Iter: 4000, Train Loss: 0.0046, Train Acc: 100.00%, Val Loss: 0.19, Val Acc: 95.26%, Time: 0:03:21 No optimization for a long time, auto-stopping...
Testing...
Test Loss: 0.12, Test Acc: 96.41%
Precision, Recall and F1-Score...
precision recall f1-score support
體育 1.00 0.99 0.99 1000
財經 0.96 0.99 0.97 1000
房產 0.98 1.00 0.99 1000
家居 0.99 0.87 0.92 1000
教育 0.93 0.94 0.93 1000
科技 0.94 0.98 0.96 1000
時尚 0.95 0.97 0.96 1000
時政 0.95 0.94 0.95 1000
游戲 0.98 0.98 0.98 1000
娛樂 0.97 0.98 0.98 1000
micro avg 0.96 0.96 0.96 10000
macro avg 0.96 0.96 0.96 10000
weighted avg 0.96 0.96 0.96 10000
Confusion Matrix...
[[991 0 0 0 5 2 0 0 2 0]
[ 0 988 2 0 2 3 0 5 0 0]
[ 0 0 997 1 1 0 0 0 0 1]
[ 1 19 19 866 18 13 30 26 4 4]
[ 0 6 2 1 937 19 7 12 8 8]
[ 0 0 0 2 2 983 4 3 6 0]
[ 1 1 0 2 8 5 973 1 2 7]
[ 0 13 0 2 24 10 1 945 1 4]
[ 1 2 1 1 4 2 2 1 982 4]
[ 1 2 0 4 5 4 3 0 2 979]]
Time usage: 0:00:08
四、模型預測
從一個科技新聞和體育新聞中,各摘取了一小段文字,進行預測,結果預測為:科技、體育。
# coding: utf-8 from __future__ import print_function import os import tensorflow as tf import tensorflow.contrib.keras as kr from cnn_model import TCNNConfig, TextCNN from cnews_loader import read_category, read_vocab try: bool(type(unicode)) except NameError: unicode = str base_dir = 'data/cnews' vocab_dir = os.path.join(base_dir, 'cnews.vocab.txt') save_dir = 'checkpoints/textcnn' save_path = os.path.join(save_dir, 'best_validation') # 最佳驗證結果保存路徑 class CnnModel: def __init__(self): self.config = TCNNConfig() self.categories, self.cat_to_id = read_category() self.words, self.word_to_id = read_vocab(vocab_dir) self.config.vocab_size = len(self.words) self.model = TextCNN(self.config) self.session = tf.Session() self.session.run(tf.global_variables_initializer()) saver = tf.train.Saver() saver.restore(sess=self.session, save_path=save_path) # 讀取保存的模型 def predict(self, message): # 支持不論在python2還是python3下訓練的模型都可以在2或者3的環境下運行 content = unicode(message) data = [self.word_to_id[x] for x in content if x in self.word_to_id] feed_dict = { self.model.input_x: kr.preprocessing.sequence.pad_sequences([data], self.config.seq_length), self.model.keep_prob: 1.0 } y_pred_cls = self.session.run(self.model.y_pred_cls, feed_dict=feed_dict) return self.categories[y_pred_cls[0]] if __name__ == '__main__': cnn_model = CnnModel() test_demo = ['三星ST550以全新的拍攝方式超越了以往任何一款數碼相機', '熱火vs騎士前瞻:皇帝回鄉二番戰 東部次席唾手可得新浪體育訊北京時間3月30日7:00'] for i in test_demo: print(cnn_model.predict(i))