# PTT Chatbot Notes
> [name=謝朋諺 (Adam Hsieh)]
> [time=Wed, Jan 2, 2019]
###### tags: `NLP`
---
## Scrapy
> [Scrapy 官方文件](https://scrapy-chs.readthedocs.io/zh_CN/0.24/intro/tutorial.html)
### Scrapy PTT Python3 編碼問題
```shell=
export PYTHONIOENCODING=utf-8
```
:::danger
:fire: 若編碼不同請暫時轉為 utf-8
:::
### 開啟爬蟲專案
```shell=
scrapy startproject [爬蟲專案名稱]
```
### Scrapy 目錄架構
#### 以 PTT_detail 為例:
```graphviz
digraph ptt_detail {
pf1[label="PTT_detail"]
pf2[label="PTT_detail"]
pf1->pf2
pf2->{items[label="items.py" fontcolor=RED] spiders middlewares[label="middlewares.py"] pipelines[label="pipelines.py"] settings[label="settings.py" fontcolor=RED]}
spiders->{ptt_detail[label="ptt_detail.py" fontcolor=RED]}
}
```
* ==ptt_detail.py== 為主要需要自己編輯的檔案,繼承自 scrapy.Spider
* **allowed_domains:** 可選。包含了 spider 允許爬取的域名 (domain) 列表 (list) 。
* **start_urls:** URL 列表。當沒有製定特定的 URL 時,spider 將從該列表中開始進行爬取。因此,第一個被獲取到的頁面的URL將是該列表之一。後續的URL將會從獲取到的數據中提取。(但我平常習慣自己做一個 urls 列表,在 parse 裡面用 for 迴圈跑,這樣才能較彈性的使用 url 處理)
* **parse:** 當 response 沒有指定回調函數時,該方法是 Scrapy 處理下載的response的默認方法。parse 負責處理response並返回處理的數據以及(/或)跟進的 URL。 Spider 對其他的Request 的回調函數也有相同的要求。該方法及其他的 Request 回調函數必須返回一個包含 Request 及(或) Item 的可迭代的對象。參數: response (Response) – 用於分析的response
:::info
:cookie: **cookies:** 用來回覆已滿 18 歲的問答。
:::
* ==items.py== 為爬蟲後建立字典的欄位,但我沒用到,主要可用建立方法比如:
* ```python=
title = scrapy.Field()
author = scrapy.Field()
date = scrapy.Field()
push = scrapy.Field()
url = scrapy.Field()
```
並在 ptt_detail.py 裡寫下要抓的目標,類似於 dictionary 寫法:
* ```python=
item['title'] = tag.css("div.title a::text")[0].extract()
item['author'] = tag.css('div.author::text')[0].extract()
item['date'] = tag.css('div.date::text')[0].extract()
item['push'] = tag.css('span::text')[0].extract()
item['url'] = tag.css('div.title a::attr(href)')[0].extract()
```
### 執行指令
```shell=
scrapy crawl [爬蟲專案名稱]
```
### 內建輸出指令(但需要 items.py 已建立)
```shell=
scrapy crawl [爬蟲專案名稱] -o output.csv
```
## Pymysql
### 連接資料庫
```python=
conn = pymysql.connect(
user="使用者帳號",
password="密碼",
host="資料庫IP",
database='資料庫名稱',
charset="編碼",
port=埠號
)
```
### 查找資料
```python=
# 使用 cursor() 方法創建一個游標對象 cursor
cursor = conn.cursor()
# 使用 execute() 方法執行 SQL 查詢
cursor.execute("SELECT * FROM [資料表名稱] WHERE [欄位]=[條件];")
# 使用 fetchone() 方法獲取單條結果
data = cursor.fetchone()
# 使用 execute() 方法執行 SQL 查詢
cursor.execute("SELECT * FROM [資料表名稱];")
# 使用 fetchall() 方法獲取全部結果
datas = cursor.fetchall()
# 關閉資料庫連接
conn.close()
```
### 輸入資料進資料庫
```python=
# 使用 cursor() 方法創建一個游標對象 cursor
cursor = conn.cursor()
# 執行 sql 語句
cursor.execute("INSERT INTO [資料表名稱] ([欄位一], [欄位二], [欄位三]) VALUES ('值一', '值二', '值三');")
# 提交到資料庫執行
conn.commit()
```
:::danger
:speech_balloon: 若資料表的儲存引擎為 Innodb 一定要有 commit() 這行才會執行
:::
### 資料庫架構
![database](https://i.imgur.com/qnm9MBq.png)
* ==ptt_list==: 下載用的資料表,主要是存取之後要抓取的 url 跟備份所用。
* ==ptt_detail==: 存取 PTT 內文的重要資訊。
* ptt_detail_id: 主要鍵,會給 ptt_push 當作外來鍵,有新的資料進來便會自動往上加 1。
* ptt_title: PTT 的標題。
* ptt_author: PTT 發文作者,包含帳號跟暱稱。
* ptt_datetime: PTT 發文時間,直接以字串存入。
* ptt_board: 發文在 PTT 哪個版上。
* ptt_content: 發文內容。
* ==ptt_push==: 存取 PTT 的留言資訊。
* ptt_push_id: PTT 留言的主要鍵,有新的資料進來會自動加一。
* ptt_detail_id: 對 ptt_detail 的 table 做外來鍵,如果 ptt_detail 的某筆資料刪除,留言應該也一併要被處理。
* ptt_push_tag: 留言的標記,有分為三種:推、噓、→。
* ptt_push_author: 留言的作者。
* ptt_push_content: 留言內容。
* ptt_push_datetime: 留言時間,直接以字串存入。
## jieba 中文斷詞
> [jieba 官方 github](https://github.com/fxsjy/jieba)
### 字典
* [臺灣常用疊詞表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d2ef68d766f396ac155b497f747e8a8d694ce0a122acc6753a9f69d4c0d6e6c7ad81a7f6e47e65a2c5)
* [臺灣常用成語表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d267c3cabf7b902c9e7caa0391ffe5e8d5da944c3a677fc5aa00ff10e8eaf399cafbf4fc1477b68d9a)
* [常用題辭表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d2fa306ccf3d60c07e33549662d90f16d37af4d3f6139884e3714c4176a8adbe4fa87dd2f504dad847)
* [臺灣常用中文縮語表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d21cc805eff492a54cde97723e7765b38bcc21407d9462af5c9bfce833dadb2645aea8d1c48aaf5959)
* [外國品牌中譯對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d2f2ffd717eea130d4b45156bb9fd3585b2662abf377e7d91dfcaa5b34c366f2989e6dd9fef5bb7894)
* [外國國名中譯對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d2e55ee3dd0dbc99392e497ce06ded17fc8b73ede6c23767247da989beac2568929c3ae0bbecc51f31)
* [外國首都名中譯對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d226614ca204a0e8501ba6615f6445ec5376afd457f899b90e710c014aedf26b6401d3d77bc7beb941)
* [臺灣特有詞詞表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d2f32cd164514429447e4666bef0bae3e23be7ecce2466273806fcd37ee6dc03e5a8a19c98e431bdbd)
* [兩岸餐飲類詞語對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d27c1c9f193332787db3b03573a90a90f03c68b0513cc5b7c74b6711fe9fec8650235c0efa393eea4e)
* [兩岸交通類詞語對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d2b87e223970f0bd6eec2e112bb1b770c438fa41c9b01f9e92dd0850c95b023da4c864cf122e01d7a5)
* [兩岸資訊類詞語對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d25c44b2b51320a8f7a1c3d0282ec94cc1657623756003744bd24f4317e0acbcb074f49dbf4f85d5fa)
* [兩岸旅遊類詞語對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d211a5e2a9d9949fbb90a4394b77afeccbb067a6813a6b05f10b0391d085cad6d48a0eb02c446c5026)
* [兩岸生活類詞語對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d239a6f9a89db718fc768ff3b10659decfac8e91a7107fe5df4e891da827d22f8ed47dd52bd89e3018)
* [兩岸地名古今對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d2b85bd1d86a7acc917719aab62c0374efdbb0e6d57fa0a39417c86576d622110294cf93b0e246036e)
* [臺灣夜市常見小吃中英對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d24a6b8a9b0b868307184d005f720361a58c009e1453730b357fc5f9785d6f2175e44995c12e0529c5)
* [臺灣主要節慶中英對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d20606ef70d55c5dc584042aef9d9093bc9ce4ff53c9a24eded962ef2bbd821df48467665d8d20a41a)
* [常見職稱中英對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d2f27363ea16441705b564c0c7c1433e0c65e7b613517f33f3c5a19aa4c492f0c00ff61da8a25efe9a)
* [親屬稱謂中英對照表](http://data.chinese-linguipedia.org/content/application/datapedia/metadata/guest-cnt-browse.php?vars=7f8ee7ff939cad426ba349a01baca255f625365a8ae284d2a49854c70d6b098913330fb489952c85b546cceaedd854ba4fe0df26c16697136b4e24dabcf07d25)
* [中文維基百科標題](https://dumps.wikimedia.org/zhwiki/)
:::warning
:speech_balloon: 中文維基百科標題只選擇3~7個字的進入字典檔,由於中英文混雜情況嚴重,以及各種符號型單詞,因此有一個小小判定若第一個 word 在 unicode 中非 u'\u4e00 ~ u'\u9fff 將使之略過
:::
以上是自己新增的字典檔,字典量有:**1,080,615**
外加上中央研究院資訊科學所詞庫小組的詞庫 dict_zh.txt 大概有 **308,431** 數量
全部字典檔合起來大概有 **14,142,679** 個詞量
### 結巴使用方法
```python=
# 引入 jieba,如果是 github 上載下來的,請記得放在同目錄下
import jieba
# 設定內建的字典檔
jieba.set_dictionary('jieba_folder/extra_dict/dict_zh.txt')
# 加入自訂的字典檔
jieba.load_userdict("jieba_folder/extra_dict/dict_new.txt")
# 使用多個執行緒,在 apple 上建議只用 4 個
jieba.enable_parallel(4)
# 開始斷詞,cut_all 為 False 代表精確模式,True 為全模式
sentence_jieba = jieba.cut(sentence, cut_all=False)
# 將斷詞結果以空格合併為字串
sentence_result = ' '.join(sentence_jieba)
```
## Word2vec
> [gensim 官網](https://radimrehurek.com/gensim/models/word2vec.html#module-gensim.models.word2vec)
### 訓練
| Parameter|size|window size| sg|alpha|min_count|workers|iter|
|-----------:|---:|----------:|---:|----:|--------:|------:|---:|
|**Default**| 100| 5|CBOW|0.025| 5| 3| 5|
|**Setting**| 300| 5|CBOW|0.025| 5| 3| 5|
上面是 Word2Vec 訓練函式中最主要的參數,本專案只更改了 Vector Size
* ==size==: 向量的維度
* ==window size==: 句子中預測和當前 word 之間最遠距離
* ==sg==: 1 代表演算法設為 skip-gram,0 則是 CBOW
* ==alpha==: 初始的 learning rate
* ==min_count==: 此單詞如果出現次數低於此值,將會不列入計算
* ==workers==: 用多少的執行緒執行
* ==iter==: 迭代次數
:::info
:fire::speech_balloon: 依照 Tomas Mikolov 的論文中提到 CBOW 模型會有較好的 syntactic accuary,而 Skip-gram 模型會有較好的 semantic accuary;而 TensorFlow 的平台上提到 CBOW 模型在小資料上就可有好的表現,但 Skip-gram 模型必須在較大的資料集才會有好的表現
:::
```python=
from gensim.models import word2vec, Word2Vec
# sentences 資料結構為
# [['歐幾裡', '得'],
# ['西元', '前三世紀', '的', '古希臘數學', '家'],
# ['現在', '被', '認為', '是', '幾何', '之父']]
# 已被斷詞切開為 list
# 開始訓練
word2model = word2vec.Word2Vec(sentences, size=300)
# 保存模型,以便重用
word2model.save("corpus_wiki_ptt_push.model")
# 載入模型
word2model = Word2Vec.load("corpus_wiki_ptt_push.model")
```
### 實際應用
輸出相近詞
```python=
# 找出相近詞,預設前十名,可設置 topn 即可設定輸出前幾名
print(word2model.wv.similar_by_word('小英'))
print(word2model.wv.most_similar('館長'))
```
結果
```
[('蔡英文', 0.8838791847229004), ('空心菜', 0.781982421875), ('賴清德', 0.7640219926834106), ('蔡', 0.7350651025772095), ('蔡總統', 0.7305333018302917), ('柯P', 0.7296426296234131), ('馬英九', 0.7275056838989258), ('dpp', 0.7234020233154297), ('DPP', 0.7211335897445679), ('蔡政府', 0.712647557258606)]
[('陳之漢', 0.6845790147781372), ('三斤', 0.6147425770759583), ('柯P', 0.6084757447242737), ('統神', 0.5917705297470093), ('呱吉', 0.5852810144424438), ('柯文哲', 0.5807035565376282), ('KKC', 0.5691466927528381), ('陳沂', 0.5669381022453308), ('段宜康', 0.5649300813674927), ('王世堅', 0.5588330030441284)]
```
比對句子相似程度
```python=
str1 = '今天天氣真好'
str2 = '今天天氣一直下雨好焦躁'
str3 = '今天天氣真糟'
str1 = jieba.cut(str1, cut_all=False)
str1 = ' '.join(str1)
str2 = jieba.cut(str2, cut_all=False)
str2 = ' '.join(str2)
str3 = jieba.cut(str3, cut_all=False)
str3 = ' '.join(str3)
print('str1: ', str1)
print('str2: ', str2)
print('str3: ', str3)
print()
print('str1 跟 str2 的距離: ', word2model.wv.wmdistance(str1,str2))
print('str1 跟 str3 的距離: ', word2model.wv.wmdistance(str1,str3))
print('str2 跟 str3 的距離: ', word2model.wv.wmdistance(str2,str3))
print()
print('str1 跟 str2 的 cosine similarity: ', word2model.wv.n_similarity(str1.split(), str2.split()))
print('str1 跟 str3 的 cosine similarity: ', word2model.wv.n_similarity(str1.split(), str3.split()))
print('str2 跟 str3 的 cosine similarity: ', word2model.wv.n_similarity(str2.split(), str3.split()))
```
結果
```
str1: 今天 天氣 真好
str2: 今天 天氣 一直 下雨 好 焦躁
str3: 今天 天氣 真糟
str1 跟 str2 的距離: 11.43087864355426
str1 跟 str3 的距離: 3.415383523379702
str2 跟 str3 的距離: 12.891501548419308
str1 跟 str2 的 cosine similarity: 0.821362
str1 跟 str3 的 cosine similarity: 0.899028
str2 跟 str3 的 cosine similarity: 0.832933
```
## QA PAIR
> [Pycon2017 chatbot 鄉民聊天機器人](https://www.slideshare.net/RyanChao3/pycon2017-chatbot)
引用連結內作者的做法,透過比對 PTT 標題跟留言的相似程度,超過閾值者當作訓練資料 QA Pair,他們比對方法使用的是 BM25 跟 POS-TAG,本專案則使用 wmdistance 的函式去預測兩句的距離。
```python=
ptt_pairs_re = list()
for i, ptt_pair in enumerate(ptt_pairs_li):
try:
str1 = jieba.cut(ptt_pair[0], cut_all=False)
str1 = ' '.join(str1)
str2 = jieba.cut(ptt_pair[1], cut_all=False)
str2 = ' '.join(str2)
if word2model.wv.wmdistance(str1,str2) < 20:
# print(ptt_pair, word2model.wv.wmdistance(str1,str2))
ptt_pairs_re.append(ptt_pair)
if i % 10000 == 0:
print('已跑至: ', i, 'QA Pair 已收錄: ', len(ptt_pairs_re))
except Exception as e:
print(e)
```
待 word2vec 訓練完畢,將字串比對的句子透過結巴斷詞完後經過 wmdistance 可比對兩組字串之間的距離相似程度
## Training
```
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
Encoder_Input (InputLayer) (None, None) 0
__________________________________________________________________________________________________
Decoder_Input (InputLayer) (None, None) 0
__________________________________________________________________________________________________
Encoder_Embedding (Embedding) (None, None, 300) 193872300 Encoder_Input[0][0]
__________________________________________________________________________________________________
Decoder_Embedding (Embedding) (None, None, 300) 193872300 Decoder_Input[0][0]
__________________________________________________________________________________________________
Encoder_LSTM (LSTM) [(None, 300), (None, 721200 Encoder_Embedding[0][0]
__________________________________________________________________________________________________
Decoder_LSTM (LSTM) (None, None, 300) 721200 Decoder_Embedding[0][0]
Encoder_LSTM[0][1]
Encoder_LSTM[0][2]
__________________________________________________________________________________________________
Decoder_Output (Dense) (None, None, 646241) 194518541 Decoder_LSTM[0][0]
==================================================================================================
Total params: 583,705,541
Trainable params: 195,960,941
Non-trainable params: 387,744,600
__________________________________________________________________________________________________
Train on 3759949 samples, validate on 417773 samples
```
646,241 的字典檔導致只能跑 4 batch size 且一個 epoch 258 小時的訓練,雖然 Loss 經過 18 小時就降到 0.3018