数据科学与工程导论期末大作业
毕竟也是投入了期末绝大部分精力和一个多月时间完成的大作业,还是值得回顾和梳理一下的。
选题和灵感
因为要和机器学习挂钩,同时也希望多少能提前接触一些未来可能的发展内容,考虑要在机器学习的几个比较热门大方向比如推荐系统、机器视觉,自然语言处理里选一个,最终决定选择自然语言处理,也就是NLP。其实这也跟我一直以来都对“让机器代替自己说话来避免无意义的闲聊”这样的事比较感兴趣有关=_=
大方向确定了,经过反复的思考和尝试,我首先在kaggle上找到一个nlp入门的contest,参考了很多现成代码和文章,对nlp有了一个基本概念,接着决定然后自己爬一些游戏评测的数据,稍加改动,得出一些尽可能有趣的结论。
作业在难度和工作量上弹性还是给的很足,按最低要求来说只要在kaggle上找一个数据集做些可视化就算完成了,但出于自我要求,还是决定尽可能加大工作量。
当然,即使花了不少时间,由于对机器学习底层模型本身一无所知,整个项目过程中几乎不涉及模型本身的实现,主要通过sklearn和keras这两个现成的包来实现模型,把主要的精力放在了数据集的选择以及对自然语言处理整体方法的了解上。
发散尝试阶段
最开始关注的是一个kaggle上的数据比赛,只需要通过机器学习模型,判断一句话是否与自然灾害有关,并根据预测的准确率估分,确实是非常基础非常入门的比赛。但通过这个比赛我也基本明确了我在整个项目中所将采用的模型和实现方法。
慢慢开始有了几个比较关注的模型和方法:Tf-idf,LSTM,还包括一些经过预训练的模型,比如BERT,但由于使用tensorflow版本问题没有进一步尝试。逐渐把重心放在sklearn和keras上。
参考:
游戏数据获取
首先想到的是steam市场,毕竟我也是老steam用户了,运气比较好的是steam对爬虫还是比较友好的,也算是个不错的用来熟悉怎么写python的练手任务。
爬取steam热销榜,获取商店页面链接
# 用于获取steam中游戏商店页面的链接,为进一步爬取评测数据做准备
import pandas as pd
from bs4 import BeautifulSoup
import re
import requests
page = 10
def getGameList(page):
linkList = []
IDList = []
nameList = []
print("正在爬取第 {} 页... 共 {} 页".format(page, totpage))
try:
req = requests.get('https://store.steampowered.com/search/?ignore_preferences=1&category1=998&os=win&filter=globaltopsellers&page=%d'%page, timeout = 10)
except:
print("again-1")
try:
req = requests.get('https://store.steampowered.com/search/?ignore_preferences=1&category1=998&os=win&filter=globaltopsellers&page=%d'%page, timeout = 10)
except:
print("again-2")
try:
req = requests.get('https://store.steampowered.com/search/?ignore_preferences=1&category1=998&os=win&filter=globaltopsellers&page=%d'%page, timeout = 10)
except:
print("again-3")
try:
req = requests.get('https://store.steampowered.com/search/?ignore_preferences=1&category1=998&os=win&filter=globaltopsellers&page=%d'%page, timeout = 10)
except:
print("FAULT!")
soup = BeautifulSoup(req.text, 'lxml')
soups = soup.find_all(href=re.compile(r"https://store.steampowered.com/app/"),class_="search_result_row ds_collapse_flag")
for i in soups:
i = i.attrs
i = i['href']
link = re.match('https://store.steampowered.com/app/(\d*?)/',i).group()
ID = re.match('https://store.steampowered.com/app/(\d*?)/(.*?)/', i).group(1)
name = re.match('https://store.steampowered.com/app/(\d*?)/(.*?)/', i).group(2)
linkList.append(link)
IDList.append(ID)
return linkList, IDList
def getdf(page):
linkList, IDList = getGameList(page)
df = pd.DataFrame(list(zip(IDList, linkList)),
columns = ['ID', 'Link'])
return df
if __name__ == '__main__':
pagestart = 1
totpage = pageend = 500
for i in range(pagestart, pageend + 1):
path = 'raw_data/steam_link/page%s.csv' % str(i)
df = getdf(i)
df.to_csv(path, index=0)
爬取商店页面内容并保存 见./crawler/crawler_st.py
简单浏览一下可以发现steam商店页面可爬取的数据还是不少的。好评率,销量,近30天的好评率,近30天的销量,标签,价格,支持的语言数,等等,参考了另一个steam游戏市场分析的文章,把爬取下来的数据做一下可视化,视觉效果似乎还行。
图很好看,爬虫写的很舒服,然后问题来了,这跟NLP有啥关系?而且这好像也不太容易带进机器学习模型吧?
没关系不要紧,我们可以想办法找一些关系嘛。(实际上,整个项目过程中我都在为了把各个一拍脑袋做出来的部分强行勾连上关系而费尽心机)
注意到一部分游戏的steam商店里有一个metacritic评分,坦白讲我之前都不知道metacritic是个啥,点进去一看才直到原来是一个专门做游戏和电影评测的网站,而且很容易就找到大量用语规范的英文游戏评测内容。到这时候,我终于知道我该选什么数据集了。
接下来就是爬取metacritic的数据集了,相比较而言,这在时间和精力上都比steam数据爬取要困难得多。心疼我为了爬数据几天晚上都没关过机的电脑(以及等它爬了一整夜后发现爬虫报错的我)
总之它最后的形态大概是这样的。
这部分的参考:
机器学习模型
有数据就可以把数据带进模型里尝试了。这里我首先对问题进行了一次相当大的简化,虽然爬到的评测都有具体的分数,但我并不关心它具体多少分,而是把超过一定分数的都标为正面样本,低于一定分数的都标为负面样本,至于在平均分附近的数据就直接剔除。
还有一些其他的处理,这一部分可以总结如下图。
简而言之,我把它转化为了一个二分类问题。这符合我在发散阶段所尝试过的问题形式,而我对此也已找到一种求解方式。
为什么呢?从计算思维的角度来说,问题规约是相当重要的。
即使文本内容不同,数据内容不同,但保证所有输入的形式一致,就保证了过程的正确性。
毕竟,在创造有价值的代码之前,第一步先要让它能够运行。一段不能运行的代码是没有任何价值的!
以LSTM模型构建为例,详见./main.py
# 设置和训练模型
def dataLoader(data, Dict, maxWords):
X = []
Y = []
for index, row in data.iterrows():
review = row[textstr]
score = row[scorestr]
if score <= bounddn:
X.append(review)
Y.append(0)
if score >= boundup:
X.append(review)
Y.append(1)
vocab_size = len(Dict)
label_size = 2
x = [[Dict[word] for word in sent.split()] for sent in X]
X = sequence.pad_sequences(x, maxlen = maxWords)
Y = np_utils.to_categorical(Y)
train_x, test_x, train_y, test_y = train_test_split(X, Y, test_size = 0.4, random_state = 242)
test_x, valid_x, test_y, valid_y = train_test_split(test_x, test_y, test_size = 0.5, random_state = 242)
return train_x, test_x, valid_x, train_y, test_y, valid_y, vocab_size, label_size
def createModel_LSTM(units, input_shape, output_dim, maxWords, vocab_size, label_size):
model = Sequential()
model.add(Embedding(input_dim = vocab_size + 1,
output_dim = output_dim,
input_length = maxWords,
mask_zero = True))
model.add(LSTM(units, return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(units, return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(units, input_shape = input_shape))
model.add(Dropout(0.2))
model.add(Dense(label_size, activation='softmax'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
model.summary()
return model
def modelTrain_LSTM(data, Dict):
maxWords = 1000
train_x, test_x, valid_x, train_y, test_y, valid_y, vocab_size, label_size= dataLoader(data, Dict, maxWords)
input_shape = (train_x[0], train_x[1])
print(input_shape)
print(train_y)
units = 100
batch_size = 32
output_dim = 20
epochs = 5
model = createModel_LSTM(units, input_shape, output_dim, maxWords, vocab_size, label_size)
getMeanScore(test_x, test_y)
print(train_y)
model.fit(train_x, train_y, epochs = epochs, batch_size = batch_size, validation_data=(valid_x, valid_y))
print(model.evaluate(test_x, test_y))
return model
如上,我尝试了大约四种模型,自然的,带入了我自己爬取的评测文本后,顺利得到了结果。
但正确率却只有60%。
60%是什么概念呢,就是如果这个模型把每个样本都预测为正样本,它就已经有50%正确率了。
乐观的说,it does work! 它当然是有在进行预测的。但和IMDb的电影评论数据集接近90%的正确率相比,显然并不理想。
嗯,只是能运行起来而已,这要求未免也太低了吧。
数据预处理和清洗
想必此时是最迷茫与沮丧的时刻,通过保证形式的一致来确保过程无误的取巧方式在这种时候已经派不上用场了。已知领域已被探索得七七八八,若想再进一步,就不得不迈向未知。
我不能说最后的解决方案有多么巧妙,甚至这样的解决方法是相当显然,且又不够严谨的。但这个凭借自我探究并取得成功的过程,于我个人来说意义非凡。
通过可视化,分析我爬取的数据集和IMDb数据集的不同之处。
一个很明显的问题是,我爬的句子太短了,大概长度只有电影评论的十分之一不到。
其实我没有花太多时间就意识到了。这很显然,事实上,这也的确是问题所在。
但是当时的我根本不敢确定这是否就是导致正确率低的原因,因为未知因素太多。会不会是模型写错了,多加一层少加一层,改一改参数,对模型的结果有多大影响?会不会是因为游戏评测的用词和评分根本就没有多少因果关系,语料本身就不适合做情感分析?
不知道,只有一个一个解决了之后才能排除掉。太短了怎么办呢,拼起来不就长了吗。把同一个游戏下多条评测合并成一句话,然后评分取平均值。
只是运气比较好,很快就找到了问题所在。
确实,不那么严谨,但逻辑上讲得通,这就够了。
准确率的提升是明显的,但明显结果不够稳定,准确率波动很大,不仅体现在不同模型的准确率差异大,即使同一模型同一参数,多次运行的结果差距也相当大,尤其是LSTM模型,准确率相差可达20%。
这一步的解决对我来说并不需要花太多时间思考,但不幸的是,对我的电脑来说就不一样了。
花了将近一天一夜时间,把metacritic上几乎所有游戏评测都爬取了下来。接近20000个游戏,每条游戏平均有30多条评测文本。
不过结果很满意就是了。
可信度分析
至此,从我的角度来说,这个项目已经可以临近尾声了,但从一个作业的角度来说,恐怕只是刚刚开始。
原因也很简单,几乎所有预先需要解决的问题在技术层面都已经打通了,无论是NLP的模型还是适合做NLP二分类模型的数据集特点,我都有了一个大致的认识和了解,这是我想到达到的目标。但作业总是希望你有头有尾,介绍你的起因和动机,最后再配一个看上去有创新点的结论(无论它是否真的有趣),过程呢反而没有那么重要。
所以,我要把我零零散散做过的东西勾连起来,构建一个相对完整的项目。这是个比较痛苦的过程,类似于总结归纳,但其实也还是挺有意义的,这会让我觉得自己好像真的做了些有实际意义的东西出来似的。
那么就开始吧,其实要构建联系并不是很困难的,最简单的方法回到了之前反复提到的,保持形式一致这个万能方法上。于是我决定用二分类的方式思考,把可信度转化成可信和不可信。
当然这里用“可信”这个说法还是容易引起歧义的,我认为评测媒体评分高于实际表现为不可信,反之,为可信。
(我知道这个说法肯定和可信这个词本身是不太符合的,但是作为玩家来说我只想知道我如果信了评测买了这个游戏会不会被坑对不对,如果我发现这游戏比评测说的还要好玩那我偷着乐都来不及,怎么会觉得评测不可信呢=_=,主要是找不到另一个恰当的词来表述了,姑且认为这个可信是被我重载过的吧!)
怎么标签呢,我觉得这个过程是非常适合可视化的,也是我在做这个项目过程中感觉为数不多的,真的在为了表达而做的可视化。
step 1: 多项式回归拟合均值曲线pred_score,引入参数偏差值 x = score - pred_score
step 2: 移除x的绝对值较小的数据点
step 3: 根据x的正负进行染色,红色表示不可信,为负样本,蓝色表示可信,为正样本
标签打好了,有了文本有了标签,代入二分类模型跑就完事了。
数据融合
这时候我终于想起了那些从steam爬来的,不知丢到哪个文件夹的数据,并开始思考怎么把它们和metacritics评测文本结合起来。
可信度,当然不只和评测者的一家之言有关,游戏的定价和本身的品质高低必然也是有影响的。
比如某个价格段的游戏可能更容易令玩家感到失望。这是非常有可能的。
那就把所有可能有关的,都放在一起逻辑回归一下咯。
# 结合游戏数据带入逻辑回归模型
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
with open('datasets/combined.csv','r',encoding='utf-8-sig')as f:
data = pd.read_csv(f)
# X = data[['price', 'param1', 'param2', 'criticScore']]
X = data[['param1','param2','price','criticScore']]
Y = data['status']
X = np.asarray(X)
Y = np.asarray(Y)
print(X)
print(Y)
train_x, test_x, train_y, test_y = train_test_split(X, Y, test_size = 0.1, random_state = 242)
model = LogisticRegression()
model.fit(train_x, train_y)
pred_y = model.predict(test_x)
print(pred_y)
pred_y2 = []
for line in test_x:
p1 = line[0]
p2 = line[1]
pred_y2.append(1 if p2 > p1 else 0)
score = accuracy_score(pred_y2, test_y)
print('仅根据文本预测的准确率: {} %'.format(score * 100))
score = accuracy_score(pred_y, test_y)
print('结合评分和定价后的准确率: {} %'.format(score * 100))
即使数据量较小,结果不太稳定,但可以看出的确有效。
结果和分析
懒得说了,直接抄PPT吧
思考和拓展
实话说整个项目虽然工作量不小,但是回头仔细一看根本没啥深度,甚至有的地方也不够严谨。只能说还算是个合格的入门项目吧。
而且值得思考和拓展的地方其实相当多。
首先,回到问题的第一步,可以发现第一步我就把它做了巨大的简化,即评测和评分其实是一个量化的指标,但我直接简化成了二分类。其实把所有的数据都量化,然后最后得到的可信度也是一个量化的指数,这听上去完全可行。
其次,由于对模型底层原理一无所知,我忽略了绝大多数参数的调试,包括模型的损失函数、评估函数等等的选择,也是按照其他人常用的来的,因此造成了不少困扰,也是我进一步完善项目最大的阻碍。还是希望下学期可以好好学底层算法,该会造的轮子还是要自己造一遍。
最后,虽然并不指望现在就能派上什么用场,但不得不说整个可信度评估框架起码还是合理的,没有非常大的逻辑漏洞,而且其实也不仅仅适用于游戏,各种商品的评测和评论也是能在形式一致的情况下套用上去的。当然,结果的正确性还是有待进一步研究。个人感觉还是值得再反复琢磨一下,在此基础上结合以后学到的东西,做一些真正有实用价值的东西也是有可能的。
话说回来,最重要的当然还是靠写项目多写了一点python,比如一些常用的包、爬虫和可视化工具之类的,以后总算也可以说自己会写python了=_=
最后附github链接 https://github.com/liyunfan1223/DaSE_Final
嗯。。希望下一次写项目可以把代码写的整洁一点,这次过程中写的很多代码因为太乱都已经找不到了,而且确实非常影响后期的整合工作。这次的话看在是第一次写python项目的份上就算啦。~