在这里,我们将实现对文本的预处理工作,下面所有的代码都可以在https://github.com/ChenglongChen/Kaggle_HomeDepot中找到。
加载一些必要的包:
import csv
import imp # 这个模块是用来实现import方法的,比如可以实现释放全局的import琐
# 意思是我可以将给定路径的.py文件以包的形式加载
import nltk
import regex # 这是一个比re更强大的正则表达式的包
import numpy as np
import pandas as pd
import multiprocessing # 实现多进程
from bs4 import BeautifulSoup
from collections import Counter
对于imp包,我们可以看看这样一个例子:这里的color_data.py是的一个Python文件,而COLOR_LIST是写在我们文件里的一个列表,这里的司仪就是我们.py文件当做模块读入,然后我们能直接访问里面的变量。
color_data=imp.load_source("","G:/kaggle/nlp/home_depot/Kaggle_HomeDepot-master/Data/dict/color_data.py")
color_data.COLOR_LIST
1 pattern_replace基类
我们初始化一个类,传入一个pattern_replace_pair_list,把满足pattern(正则)的替换为replace(tuple格式),并且去掉开头结尾的空格。
class BaseReplacer:
def __init__(self,pattern_replace_pair_list=[]):
self.pattern_replace_pair_list=pattern_replace_pair_list
def transform(self,text):
for patter,replace in self.pattern_replace_pair_list:
try:
text=regex.sub(pattern,replace,text)
except:
pass
return regex.sub(r"\s+"," ",text).strip()
我们下面要将这个类作为父类,对文本进行一些处理,包括:
- 移除括号中的单词或用点替换括号
- 移除数字之间的逗号(比如10,000转换成10000)
- 在数字(左)和字母(右)之间添加点和空格
- 如果一个数字的左边有至少3个字母,则在数字和字母之间增加空格
- 在字母之间用空格替换"/"和"\"
- 由于产品描述是文本链而成的,所以会导致像firstSecond这样的词出现,要用正则表达式将其分开
- 等等。。。。。。。。。。
1.1 大写转换为小写
class LowerCaseConverter(BaseReplacer):
"""
Traditional -> traditional
"""
def transform(self,text):
return text.lower()
1.2 将文本中本应该分开的词分开
class LowerUpperCaseSplitter(BaseReplacer):
def __init__(self):
self.pattern_replace_pair_list=[
(r"(\w)[\.?!]([A-Z])",r"\1 \2"),
(r"(?<=( ))([a-z]+)([A-Z]+)",r"\2 \3"),
]
这里使用了正则表达式,不了解正则表达式的同学可以看看http://www.regexlab.com/zh/regref.htm,或者自行百度。
这里主要将如“firstSecond”或者“first.Second”的形式替换成了“first Second”。可以看看如下实验:
lp=LowerUpperCaseSplitter()
text="hidden from viewDurable rich finishLimited lifetime warrantyEncapsulated panels"
print(lp.transform(text))
text="product.Excellent"
print(lp.transform(text))
输出:
hidden from view Durable rich finish Limited lifetime warranty Encapsulated panels
product Excellent
1.3 进行一些单词的替换
这些替换包括了如果如fisrtsecond => first second,并且所给的数据中search term里面存在大量的拼写错误,我们要进行替换,还包括拓展了一些单词的缩写以及一些单词的stemming,lemma等等。
# 我们传入一个我们字典的路径
class WordReplacer(BaseReplacer):
def __init__(self,replace_name):
self.replace_name=replace_name
self.pattern_replace_pair_list=[]
for line in csv.reader(open(self.replace_name)):
if len(line)==1 and line[0].startswith("#"):
continue
try:
pattern=r"(?<=\W|^)%s(?=\W|$)" % line[0]
replace=line[1]
self.pattern_replace_pair_list.append((pattern,replace))
except:
print(line)
pass
其中正则表达式表示r"(?<=\W|^)%s(?=\W|$)",我们所要替换的单词的左侧必须是非字母非数字,或者是字符串的开头,右侧必须是非字母非数字,或者是字符串的结尾。
例子:
# 单词拆分
wr=WordReplacer("G:/kaggle/nlp/home_depot/Kaggle_HomeDepot-master/Data/dict/word_replacer.csv")
text="zeroturn"
print(wr.transform(text))
# 拼写纠正
text="repir"
print(wr.transform(text))
输出:
zero turn
repair
1.3.1 关于单词拼写纠错
关于单词拼写纠错,这里有一个很有意思的做法是https://www.kaggle.com/steubk/fixing-typos/notebook,大概的思路就是使用google搜索的时候,如果所填的单词是错误的,那么google就会帮忙纠正,就像这样
然后这可以用爬虫爬下来,然后国内要科学上网。。。。所以我尝试着用requests+xpath写了的百度的版本,发现很鸡肋很鸡肋。。。以后有时间再看看。
import requests
from lxml import etree
import time
from random import randint
# 首先是一个单词纠错
def fixing_typos(q):
time.sleep(randint(0,2))
url="https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=baidu&wd="+q
headers={'User-Agent':'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0'}
response=requests.get(url,headers=headers)
html=etree.HTML(response.text)
new_word=html.xpath('//div[@class="c-gap-bottom-small f13"]/span/strong[2]/text()')[0]
return new_word
if __name__=="__main__":
print(fixing_typos("abaut"))
1.4 分割连接符连接的单词
去掉字母之间的连接符,但是不会去掉数字之间的
class LetterLetterSplitter(BaseReplacer):
def __init__(self):
self.pattern_replace_pair_list=[
(r"([a-zA-Z]+)[/\-]([a-zA-Z])",r"\1 \2")
]
例子:
lls=LetterLetterSplitter()
text="Cleaner/Conditioner"
print(lls.transform(text))
输出:
Cleaner Conditioner
1.5 去掉字母和数字间的连接符
class DigitLetterSplitter(BaseReplacer):
def __init__(self):
self.pattern_replace_pair_list=[
(r"(\d+)[\.\-]([a-zA-Z]+)",r"\1 \2"),
(r"([a-zA-Z]+)[\.\-](\d+)",r"\1 \2"),
]
例子:
dlp=DigitLetterSplitter()
text="1-Gang"
print(dlp.transform(text))
输出:
1 Gang
1.6 去掉数字之间的逗号
class DigitCommaDigitMerger(BaseReplacer):
def __init__(self):
self.pattern_replace_pair_list=[
(r"(?<=\d+),(?=000)",r"")
]
例子:
dcdm=DigitCommaDigitMerger()
text="1,000,000"
print(dcdm.transform(text))
输出:
1000000
1.7 将英语形式的number变换为阿拉伯数字
class NumberDigitMapper(BaseReplacer):
def __init__(self):
numbers = [
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
"eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen",
"nineteen", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"
]
digits = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 30, 40, 50, 60, 70, 80, 90
]
self.pattern_replace_pair_list=[
(r"(?<=\W|^)%s(?=\W|$)" % n,str(d)) for n,d in zip(numbers,digits)
]
例子:
ndm=NumberDigitMapper()
text="two"
print(ndm.transform(text))
输出:2
1.8 统一单位
class UnitConverter(BaseReplacer):
"""
shadeMature height: 36 in. - 48 in.Mature width
PUT one UnitConverter before LowerUpperCaseSplitter
"""
def __init__(self):
self.pattern_replace_pair_list = [
(r"([0-9]+)( *)(inches|inch|in|in.|')\.?", r"\1 in. "),
(r"([0-9]+)( *)(pounds|pound|lbs|lb|lb.)\.?", r"\1 lb. "),
(r"([0-9]+)( *)(foot|feet|ft|ft.|'')\.?", r"\1 ft. "),
(r"([0-9]+)( *)(square|sq|sq.) ?\.?(inches|inch|in|in.|')\.?", r"\1 sq.in. "),
(r"([0-9]+)( *)(square|sq|sq.) ?\.?(feet|foot|ft|ft.|'')\.?", r"\1 sq.ft. "),
(r"([0-9]+)( *)(cubic|cu|cu.) ?\.?(inches|inch|in|in.|')\.?", r"\1 cu.in. "),
(r"([0-9]+)( *)(cubic|cu|cu.) ?\.?(feet|foot|ft|ft.|'')\.?", r"\1 cu.ft. "),
(r"([0-9]+)( *)(gallons|gallon|gal)\.?", r"\1 gal. "),
(r"([0-9]+)( *)(ounces|ounce|oz)\.?", r"\1 oz. "),
(r"([0-9]+)( *)(centimeters|cm)\.?", r"\1 cm. "),
(r"([0-9]+)( *)(milimeters|mm)\.?", r"\1 mm. "),
(r"([0-9]+)( *)(minutes|minute)\.?", r"\1 min. "),
(r"([0-9]+)( *)(°|degrees|degree)\.?", r"\1 deg. "),
(r"([0-9]+)( *)(v|volts|volt)\.?", r"\1 volt. "),
(r"([0-9]+)( *)(wattage|watts|watt)\.?", r"\1 watt. "),
(r"([0-9]+)( *)(amperes|ampere|amps|amp)\.?", r"\1 amp. "),
(r"([0-9]+)( *)(qquart|quart)\.?", r"\1 qt. "),
(r"([0-9]+)( *)(hours|hour|hrs.)\.?", r"\1 hr "),
(r"([0-9]+)( *)(gallons per minute|gallon per minute|gal per minute|gallons/min.|gallons/min)\.?", r"\1 gal. per min. "),
(r"([0-9]+)( *)(gallons per hour|gallon per hour|gal per hour|gallons/hour|gallons/hr)\.?", r"\1 gal. per hr "),
]
1.9 消除Html标签
这里使用了bs4解析文本消除html标签,这里用了自己定义的parser,我之前在用的是bs4里面的html.parser
# 我们使用bs4解析html消除标签
class HtmlCleaner:
def __init__(self,parser):
self.parser=parser
def transform(self,text):
bs=BeautifulSoup(text,self.parser)
text=bs.get_text(separator=" ")
return text
1.10 将一些乱码和标点替换成原始标点
class QuartetCleaner(BaseReplacer):
def __init__(self):
self.pattern_replace_pair_list = [
(r"<.+?>", r""),
# html codes
(r" ", r" "),
(r"&", r"&"),
(r"'", r"'"),
(r"/>/Agt/>", r""),
(r"</a<gt/", r""),
(r"gt/>", r""),
(r"/>", r""),
(r"<br", r""),
# do not remove [".", "/", "-", "%"] as they are useful in numbers, e.g., 1.97, 1-1/2, 10%, etc.
(r"[ &<>)(_,;:!?\+^~@#\$]+", r" "),
("'s\\b", r""),
(r"[']+", r""),
(r"[\"]+", r""),
]
1.11 词形还原(lemma)和词根提取(stem)
在做lemmatizing的时候,我们首先使用nltk包中的宾州Tree Bank分词器进行分词(nltk中多个分词器的效果对比https://blog.csdn.net/zhuzuwei/article/details/80485318),然后使用wordnet词形还原器进行词形还原。
class Lemmatizer:
def __init__(self):
self.Tokenizer=nltk.tokenize.TreebankWordTokenizer()
self.Lemmatizer=nltk.stem.WordNetLemmatizer()
def transform(self,text):
tokens=[self.Lemmatizer.lemmatize(word) for word in self.Tokenizer.tokenize(text)]
return " ".join(tokens)
在做stemming的时候,这里可以选择nltk中的porter或者snowball进行词根提取(其实感觉这两个效果差不多)
# 词根提取
class Stemming:
def __init__(self,stemmer_type="snowball"):
self.stemmer_type=stemmer_type
if self.stemmer_type=="porter":
self.stemmer=nltk.stem.PorterStemmer()
elif self.stemmer_type=="snowball":
self.stemmer=nltk.SnowballStemmer("english")
def transform(self,text):
tokens=[self.stemmer.stem(word) for word in text.split(" ")]
return " ".join(tokens)
1.12 Query和Title拓展
因为在product title中的一些单词之间的结构能够帮助我们找到文档中的重要的内容,比如从’Husky 52 in. 10-Drawer Mobile Workbench with Solid Wood Top, Black’这个title中,我们就可以知道这个产品是workbench而不是wood top。对于product title这样的短文本,使用ngram进行丰富,这里我们使用的是trigram。
首先来实现unigrams、bigrams和trigrams
# unigrams,bigrams和trigrams实现
def _unigrams(words):
assert type(words)==list
return words
def _bigrams(words,join_string,skip=0):
assert type(words)==list
L=len(words)
lst=[]
if L>1:
for i in range(L-1):
for k in range(1,skip+2):
if i+k<L:
lst.append(join_string.join([words[i],words[i+k]]))
else:
lst=_unigrams(words)
return lst
def _trigrams(words,join_string,skip=0):
assert type(words)==list
L=len(words)
lst=[]
if L>2:
for i in range(L-2):
for j in range(1,skip+2):
for k in range(1,skip+2):
if i+j+k<L:
lst.append(join_string.join([words[i],words[i+j],words[i+j+k]]))
else:
lst=_bigrams(words,join_string,skip=0)
return lst
# 更多的ngrams以此类推。。。
例子:
words=["I","am","Sun"]
# unigrams
print(_unigrams(words))
# bigrmas
print(_bigrams(words,"_",skip=0))
# trigrams
print(_trigrams(words,"_",skip=0))
输出:
['I', 'am', 'Sun']
['I_am', 'am_Sun']
['I_am_Sun']
然后来实现文本拓展,我们将文本中出现次数至少的90%,当做停用词,结合我们常用的停用词表,将这些从文本中去除,我们先对product_title使用trigrams模型对文本进行拓展,然后对于同一个搜索关键词(search_term),我们寻找其对应的出现最多的trigram结构,我们这个结构对于search_terms越重要。
class QueryExpansion:
def __init__(self,ngram=3,stopwords_threshoold=0.9,base_stopwords=set()):
self.df=df[["search_term","product_title"]].copy()
self.ngram=ngram
self.stopwords_threshoold=stopwords_threshoold
self.stopwords=set(base_stopwords).union(self._get_customized_stopwords())
# 根据所设置的阈值,将一些词设置成stopwords
def _get_customized_stopwords(self):
words=" ".join(list(self.df["product_title"].values)).split(" ")
counter=Counter(words)
num_uniq=len(counter)
num_stop=int((1.-self.stopwords_threshoold)*num_uniq)
stopwords=set()
for e,(w,c) in enumerate(counter):
if e==num_stop:
break
stopwords.add(w)
return stopwords
# 实现ngram模型
def _ngram(self,text):
tokens=text.split(" ")
tokens=[word for word in tokens if word not in self.stopwords]
return _trigrams(tokens," ")
# 寻找出现次数最多的词,我们认为词出现的次数越多,对于这个search_term越重要
def _get_alternative_query(self,df):
res=[]
for v in df:
res+=v
c=Counter(res)
value,count=c.most_common()[0]
return value
# 目标找出对于同一个search_term出现次数最多的trigram
def build(self):
self.df["title_ngram"]=self.df["product_title"].apply(self._ngram)
corpus=self.df.groupby("search_term").apply(
lambda df:self._get_alternative_query(df["title_ngram"]))
corpus=corpus.reset_index()
corpus.columns=["search_term","search_term_alt"]
self.df=pd.merge(self.df,corpus,on="search_term",how="left")
return self.df["search_term_alt"].values
1.13 处理product_title
# 首先定义一些正则表达式
color_data=imp.load_source("","G:/kaggle/nlp/home_depot/Kaggle_HomeDepot-master/Data/dict/color_data.py")
COLORS_PATTERN=r"(?<=\W|^)%s(?=\W|$)" % "|".join(color_data.COLOR_LIST)
UNITS=[" ".join(r.strip().split(" ")[1:]) for p,r in UnitConverter().pattern_replace_pair_list]
UNITS_PATTERN = r"(?:\d+[?:.,]?\d*)(?: %s\.*)?"%("|".join(UNITS))
DIM_PATTERN_NxNxN = r"%s ?x %s ?x %s"%(UNITS_PATTERN, UNITS_PATTERN, UNITS_PATTERN)
DIM_PATTERN_NxN = r"%s ?x %s"%(UNITS_PATTERN, UNITS_PATTERN)
class ProductNameExtractor(BaseReplacer):
def __init__(self):
self.pattern_replace_pair_list = [
# 去掉括号里的文本描述
("[ ]?[[(].+?[])]", r""),
# 去掉 "made in..."
("made in [a-z]+\\b", r""),
# 去掉连词符,逗号后面有空格,最多我们两个单词或数字
("([,-]( ([a-zA-Z0-9]+\\b)){1,2}[ ]?){1,}$", r""),
# Remove descriptions (prepositions staring with: with, for, by, in )
("\\b(with|for|by|in|w/) .+$", r""),
# 去掉colors和sizes
("size: .+$", r""),
("size [0-9]+[.]?[0-9]+\\b", r""),
(COLORS_PATTERN, r""),
# dimensions
(DIM_PATTERN_NxNxN, r""),
(DIM_PATTERN_NxN, r""),
# measurement units
(UNITS_PATTERN, r""),
# others
("(value bundle|warranty|brand new|excellent condition|one size|new in box|authentic|as is)", r""),
# stop words
("\\b(in)\\b", r""),
# hyphenated words
("([a-zA-Z])-([a-zA-Z])", r"\1\2"),
# special characters
("[ &<>)(_,.;:!?/+#*-]+", r" "),
# numbers that are not part of a word
("\\b[0-9]+\\b", r""),
]
# 使用我们上面定义的一系列的类进行文本处理
def preprocess(self, text):
pattern_replace_pair_list = [
# Remove single & double apostrophes
("[\"]+", r""),
# Remove product codes (long words (>5 characters) that are all caps, numbers or mix pf both)
# don't use raw string format
("[ ]?\\b[0-9A-Z-]{5,}\\b", ""),
]
text = BaseReplacer(pattern_replace_pair_list).transform(text)
text = LowerCaseConverter().transform(text)
text = DigitLetterSplitter().transform(text)
text = UnitConverter().transform(text)
text = DigitCommaDigitMerger().transform(text)
text = NumberDigitMapper().transform(text)
text = UnitConverter().transform(text)
return text
def transform(self,text):
# super()函数在这里的意思是我调用了父类BaseReplace中的transform方法
text=super().transform(self.preprocess(text))
# 词形还原和词根提取
text=Lemmatizer().transform(text)
text=Stemming(stemmer_type="snowball").transform(text)
# 取文本中的最后两个单词
text=" ".join(text.split(" ")[-2:])
return text
1.14 处理商品的属性
根据输出的不同格式写两种方法
# 根据输出的不同格式写两个方法
def _split_attr_to_text(text):
attrs=text.split(" | ")
return " ".join(attrs)
def _split_attrs_to_list(text):
attrs=text.split(" | ")
if len(attrs)==1:
return [[attrs[0],attrs[0]]]
else:
return [[n,v] for n,v in zip(attrs[::2],attrs[1::2])]
1.15 处理不同结构的数据输入输出和dataframe的多进程
一方面,不同的数据可能放在列表里,也可能放在数据框里,同时单个数据的格式可能是str,float,int,我们都需要把他们格式化为str进行处理;另一方面,我们对以列表的形式封装多个处理类。
这里使用了python中的multiprocessing实现多进程,我们使用进程池pool,然后两个异步方法Imap和map_async之间的区别可以看看这个https://stackoverflow.com/questions/26520781/multiprocessing-pool-whats-the-difference-between-map-async-and-imap。
class ProcessorWrapper:
def __init__(self,processor):
self.processor=processor
def transform(self,input):
if isinstance(input,str):
out=self.processor.transform(input)
elif isinstance(input,float) or isinstance(input,int):
out=self.processor.transform(sytr(input))
elif isinstance(input,list):
out=[0]*len(input)
for i in range(len(input)):
out[i]=ProcessorWrapper(self.processor).transform(input[i])
else:
raise(ValueError("Currently not support type: %s" % type(input).__name__))
return out
# list处理器封装,我们需要对同一个列表使用多个处理器处理的情况
class ListProcessors:
def __init__(self,processors):
self.processors=processors
def process(self,lst):
for i in range(len(lst)):
for processor in self.processors:
lst[i]=ProcessorWrapper(processor).transform(lst[i])
return lst
# dataframe处理器封装
class DataFrameProcessor:
def __init__(self,processors):
self.processors=processors
def process(self,df):
for process in self.processors:
df=df.apply(ProcessorWrapper(processor).transform)
return df
# 多进程的实现
class DataFrameParalleProcessor:
def __init__(self,processors,n_jobs=4):
self.processors=processors
self.n_jobs=n_jobs
def prosess(self,dfAll,columns):
df_processor=DataFrameProcessor(self.processors)
p=multiprocessing.Pool(self.n_jobs)
dfs=p.imap(df_processor.process,[dfAll[col] for col in columns])
for col,df in zip(columns,dfs):
dfAll[col]=df
return dfAll
2.文本预处理
写完所有处理的类,就可以正式开始文本处理了。
2.1 首先,进行一个测试
首先,我们进行一个测试,检查一下我们的代码时候起作用。
def main():
columns={
"product_attribute_concat",
"product_description",
"product_brand",
"product_color",
"product_title",
"search_term",
}
# 使用一个列表将放置要处理的进程类
processors = [
# 单位标准化
UnitConverter(),
# 将一些连在一起的词分开
LowerUpperCaseSplitter(),
# 替换一些词
WordReplacer(replace_name="G:/kaggle/nlp/home_depot/Kaggle_HomeDepot-master/Data/dict/word_replacer.csv"),
# 去掉词与此之间的/\-
LetterLetterSplitter(),
# 对数字和词之间做一些处理
DigitLetterSplitter(),
# 去掉数字之间的逗号
DigitCommaDigitMerger(),
# 将英文的数字转换成阿拉伯数字
NumberDigitMapper(),
# 再做一次单位标准化
UnitConverter(),
# 去掉文本中一些没有用的符号
QuartetCleaner(),
# 去掉文本中的html标签
HtmlCleaner(parser="html.parser"),
# 转换为小写
LowerCaseConverter(),
# 词形还原
Lemmatizer(),
]
# 词干提取
stemmers = [
Stemming(stemmer_type="snowball"),
Stemming(stemmer_type="porter")
][0:1]
# 进行一个简单的测试
text="1/2 inch rubber lep tipsBullet07 1,000,000 one"
print("Original:")
print(text)
list_processor=ListProcessors(processors)
print("After:")
print(list_processor.process([text]))
main()
输出:
Original:
1/2 inch rubber lep tipsBullet07 1,000,000 one
After:
['1/2 in. rubber lep tip bullet07 1000000 1']
这里的代码和大神的代码不同的是,我把单词小写化这步放在了词根提取的前面,因为我觉得我们做连贯的大小写单词的分离等步骤的时候都需要保持原有的大写。
2.2 使用pickle进行结构化数据的保存和加载
import pickle
def _save(fname,data,protocal=3):
with open(fname,"wb") as f:
pickle.dump(data,f,protocal)
def _load(fname):
with open(fname,"rb") as f:
return pickle.load(f)
2.3 对原有数据进行读取和连接
import gc # 垃圾回收
dfTrain=pd.read_csv("G:/kaggle/nlp/home_depot/train.csv",encoding="ISO-8859-1")
dfTest=pd.read_csv("G:/kaggle/nlp/home_depot/test.csv",encoding="ISO-8859-1")
dfAttr=pd.read_csv("G:/kaggle/nlp/home_depot/attributes.csv")
dfDesc=pd.read_csv("G:/kaggle/nlp/home_depot/product_descriptions.csv")
test_size=len(dfTest)
dfTest["relevance"]=np.zeros((test_size))
dfAttr.dropna(how="all",inplace=True)
dfAttr['value']=dfAttr.value.astype("str")
# 合并训练集和测试集
dfAll=pd.concat([dfTrain,dfTest],ignore_index=True,axis=0)
del dfTrain
del dfTest
gc.collect()
# 将dfAll和dfDesc按照id合并
dfAll=pd.merge(dfAll,dfDesc,on="product_uid",how="left")
MISSING_VALUE_STRING = "MISSINGVALUE"
dfAll.fillna(MISSING_VALUE_STRING,inplace=True)
del dfDesc
gc.collect()
# 拼接产品的品牌
dfBrand=dfAttr[dfAttr.name=="MFG Brand Name"][['product_uid','value']].rename(columns={"value":"product_brand"}) # 我们把value的列名替换成了product_brand
dfAll=pd.merge(dfAll,dfBrand,on="product_uid",how="left")
dfAll["product_brand"]=dfAll["product_brand"].values.astype("str")
dfAll.fillna(MISSING_VALUE_STRING,inplace=True)
del dfBrand
gc.collect()
# 拼接产品颜色
color_columns = ["product_color", "Color Family", "Color/Finish", "Color/Finish Family"]
dfColor=dfAttr[dfAttr.name.isin(color_columns)][["product_uid","value"]].rename(columns={"value":"product_color"})
dfColor.dropna(how="all",inplace=True)
_agg_color=lambda df:" ".join(list(set(df["product_color"])))
dfColor=dfColor.groupby(["product_uid"]).apply(_agg_color)
dfColor=dfColor.reset_index(name="product_color")
dfColor['product_color']=dfColor['product_color'].values.astype(str)
# 拼接
dfAll=pd.merge(dfAll,dfColor,on="product_uid",how="left")
dfAll.fillna(MISSING_VALUE_STRING,inplace=True)
del dfColor
gc.collect()
# 我们把同一个id的其他属性拼接在一起
_agg_attr=lambda df:" | ".join(df["name"]+" | "+df["value"])
dfAttr=dfAttr.groupby(["product_uid"]).apply(_agg_attr)
dfAttr=dfAttr.reset_index(name="product_attribute_concat")
dfAll=pd.merge(dfAll,dfAttr,on="product_uid",how="left")
dfAll.fillna(MISSING_VALUE_STRING,inplace=True)
del dfAttr
gc.collect()
# 保存数据
_save("G:/kaggle/nlp/home_depot/all.raw.csv.pkl",dfAll)
dfInfo=dfAll[["id","relevance"]].copy()
_save("G:/kaggle/nlp/home_depot/info.csv.pkl",dfInfo)
del dfAll
del dfInfo
gc.collect()
2.4 数据处理
columns_to_proc={
"product_attribute_concat",
"product_description",
"product_brand",
"product_color",
"product_title",
"search_term",
}
# 使用一个列表将放置要处理的进程类
processors = [
# 单位标准化
UnitConverter(),
# 将一些连在一起的词分开
LowerUpperCaseSplitter(),
# 替换一些词
WordReplacer(replace_name="G:/kaggle/nlp/home_depot/Kaggle_HomeDepot-master/Data/dict/word_replacer.csv"),
# 去掉词与此之间的/\-
LetterLetterSplitter(),
# 对数字和词之间做一些处理
DigitLetterSplitter(),
# 去掉数字之间的逗号
DigitCommaDigitMerger(),
# 将英文的数字转换成阿拉伯数字
NumberDigitMapper(),
# 再做一次单位标准化
UnitConverter(),
# 去掉文本中一些没有用的符号
QuartetCleaner(),
# 去掉文本中的html标签
HtmlCleaner(parser="html.parser"),
# 转换为小写
LowerCaseConverter(),
# 词形还原
Lemmatizer(),
]
# 词干提取
stemmers = [
Stemming(stemmer_type="snowball"),
Stemming(stemmer_type="porter")
][0:1]
dfAll=_load("G:/kaggle/nlp/home_depot/all.raw.csv.pkl")
columns_to_proc=[col for col in columns_to_proc if col in dfAll.columns]
# 对search_term和product_title进行进行一些提取处理
ext=ProductNameExtractor()
dfAll["search_term_product_name"]=dfAll["search_term"].apply(ext.transform)
dfAll["product_title_product_name"]=dfAll["product_title"].apply(ext.transform)
# 因为我们没办法科学上网,所以我们在单词拼写纠正的时候使用现成的字典
dicts=imp.load_source("","G:/kaggle/nlp/home_depot/my_bag/spell_check_dict.py")
def spell_check(text):
spell_dict=dicts.spell_check_dict
if text in spell_dict.keys():
text=spell_dict[text]
return text
dfAll["search_term"]=dfAll["search_term"].apply(spell_check)
# 然后我们使用前面定义的各种类对数据进行处理
df_processor=DataFrameParalleProcessor(processors)
dfAll=df_processor.process(dfAll,columns_to_proc)
# 对属性特征进行两种形式的处理
dfAll["product_attribute"]=dfAll["product_attribute_concat"].apply(_split_attr_to_text)
dfAll["product_attribute_list"]=dfAll["product_attribute_concat"].apply(_split_attrs_to_list)
# 文本拓展
list_processor=ListProcessors(processors)
from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS
# 我们对于stopword也进行上面各种操作
stopwords=set(list_processor.process(list(ENGLISH_STOP_WORDS)))
qe=QueryExpansion(dfAll,ngram=3,stopwords_threshoold=0.9,base_stopwords=stopwords)
dfAll["search_term_alt"]=qe.build()
# 保存数据
_save("G:/kaggle/nlp/home_depot/dfAll_preprocessing.pkl",dfAll)
最后的结果:
<class 'pandas.core.frame.DataFrame'> Int64Index: 240760 entries, 0 to 240759 Data columns (total 14 columns): id 240760 non-null int64 product_uid 240760 non-null int64 product_title 240760 non-null object search_term 240760 non-null object relevance 240760 non-null float64 product_description 240760 non-null object product_brand 240760 non-null object product_color 240760 non-null object product_attribute_concat 240760 non-null object search_term_product_name 240760 non-null object product_title_product_name 240760 non-null object product_attribute 240760 non-null object product_attribute_list 240760 non-null object search_term_alt 240760 non-null object dtypes: float64(1), int64(2), object(11) memory usage: 27.6+ MB
小结:
- 这里使用了父类,避免代码重复
- 使用多线程(虽然到最后我也没有实现),节省代码运行速度
- 其实作者还使用了config文件,存放一些变量和路径,防止代码看起来很乱
好的,结束了文本预处理的工作,下面要开始特征提取了,我们将尝试词袋模型,tfidf,主题模型(Lsi,Lda)和词向量嵌入(word2vec,doc2vec)等等方法提取文本特征。
一些参考:
https://www.jianshu.com/p/7f61e11d8bfc
https://blog.csdn.net/sinat_15355869/article/details/80102311
https://www.kaggle.com/c/home-depot-product-search-relevance/discussion/18967