博客荒废了很久准备重新开始写起来。

最近一直在忙毕设的事情准备用深度学习的方法进行微博情感分析,在我们的研究中,将使用5分类的方法来将微博进行分类。
之前纠结深度学习工具的选择,先后在theano,deeplearningtoolbox,torch和deeplarining4j之间纠结了很久,选来选去最终还是选了torch7。具体原因先按下不表,过程很纠结就是了。

torch7除了标准的nn包之外,还提供了dp包来进行深度学习,本文就利用了dp包进行编程。处理了固定长度(12词)的文本分类问题。不同长度的代码还在编写中。

 

  • 数据准备

数据准备方面,首先是用了word2vec工具将分词后的文本都学习成了embeded vector每个词向量长度100,将词长度为12的句子挑选出来进行采样,制作成训练集、验证集合测试集,相当于每个句子是一个1200维的向量。五个文件,分别代表5个分类。

在读取文件的时候,我把1×1200维的向量reshape成了12*100维的向量。
prepareData.lua

[code]
for _,dataset_name in ipairs({"train","valid","test"}) do
datas=nil
classes=nil
path_prefix=os.getenv(‘HOME’).."/data/weibo/"
th_output_prefix=os.getenv(‘HOME’).."/workspace/torch7/"
path_surfix=".txt"
for _,index in ipairs({0,1,2,3,4}) do
data_n={}
classes_n={}
file=io.open(path_prefix..dataset_name..index..path_surfix,’r’)
for line in file:lines() do
line_vector={}
for element in string.gmatch(line,"%S+") do
table.insert(line_vector,element)
end
table.insert(data_n,line_vector)
end
data_tensor_n=torch.Tensor(data_n)
data_tensor_n=data_tensor_n:resize(data_tensor_n:size(1),data_tensor_n:size(2)/100,100)
classes_tensor_n=torch.Tensor(data_tensor_n:size(1)):fill(index)
print(data_tensor_n:size())
print(classes_tensor_n:size())
datas=datas and torch.cat(datas,data_tensor_n,1) or data_tensor_n
classes=classes and torch.cat(classes,classes_tensor_n,1) or classes_tensor_n
end
classes=classes:int()
print(datas:size())
print(classes:size())
data_object={datas,classes}
torch.save(th_output_prefix..dataset_name..’.th7′,data_object)
end
[/code]

制作3个数据文件,分别取名为train.th7,valid.th7和test.th7。

  • datasource编写
    利用mnist的datasource改的。注意,我们需要的输入是一个SequenceView,也就是可以用来做1维卷积的View。SequenceView中的bwc分别代表“batch大小”,“句子长度”和“embedVector的大小”

[code]
local Weibo, DataSource = torch.class("dp.Weibo", "dp.DataSource")
Weibo.isMnist = true

Weibo._name = ‘weibo’
Weibo._text_axes = ‘bwc’
Weibo._classes = {0, 1, 2, 3, 4, }

function Weibo:__init(config)
config = config or {}
assert(torch.type(config) == ‘table’ and not config[1],
"Constructor requires key-value arguments")
local args, load_all, input_preprocess, target_preprocess
args, self._valid_ratio, self._train_file, self._test_file, self._valid_file,
self._data_path, self._scale, self._binarize, self._shuffle, load_all, input_preprocess,
target_preprocess
= xlua.unpack(
{config},
‘Weibo’,
‘Handwritten digit classification problem.’ ..
‘Note: Train and valid sets are already shuffled.’,
{arg=’valid_ratio’, type=’number’, default=1/6,
help=’proportion of training set to use for cross-validation.’},
{arg=’train_file’, type=’string’, default=’train.th7′,
help=’name of training file’},
{arg=’valid_file’, type=’string’, default=’valid.th7′,
help=’name of valid file’},
{arg=’test_file’, type=’string’, default=’test.th7′,
help=’name of test file’},
{arg=’data_path’, type=’string’, default=dp.DATA_DIR,
help=’path to data repository’},
{arg=’scale’, type=’table’,
help=’bounds to scale the values between. [Default={0,1}]’},
{arg=’binarize’, type=’boolean’,
help=’binarize the inputs (0s and 1s)’, default=false},
{arg=’shuffle’, type=’boolean’,
help=’shuffle different sets’, default=false},
{arg=’load_all’, type=’boolean’,
help=’Load all datasets : train, valid, test.’, default=true},
{arg=’input_preprocess’, type=’table | dp.Preprocess’,
help=’to be performed on set inputs, measuring statistics ‘ ..
‘(fitting) on the train_set only, and reusing these to ‘ ..
‘preprocess the valid_set and test_set.’},
{arg=’target_preprocess’, type=’table | dp.Preprocess’,
help=’to be performed on set targets, measuring statistics ‘ ..
‘(fitting) on the train_set only, and reusing these to ‘ ..
‘preprocess the valid_set and test_set.’}
)
self:loadTrain()
self:loadValid()
self:loadTest()
DataSource.__init(self, {
train_set=self:trainSet(), valid_set=self:validSet(),
test_set=self:testSet(), input_preprocess=input_preprocess,
target_preprocess=target_preprocess
})
end

function Weibo:loadTrain()
local train_data = self:loadData(self._train_file)
self:setTrainSet(
self:createDataSet(train_data[1], train_data[2], ‘train’)
)
return self:trainSet()
end

function Weibo:loadValid()
local valid_data = self:loadData(self._valid_file)
self:setValidSet(
self:createDataSet(valid_data[1], valid_data[2], ‘valid’)
)
return self:validSet()
end

function Weibo:loadTest()
local test_data = self:loadData(self._test_file)
self:setTestSet(
self:createDataSet(test_data[1], test_data[2], ‘test’)
)
return self:testSet()
end

function Weibo:createDataSet(inputs, targets, which_set)
if self._shuffle then
local indices = torch.randperm(inputs:size(1)):long()
inputs = inputs:index(1, indices)
targets = targets:index(1, indices)
end
if self._binarize then
DataSource.binarize(inputs, 128)
end
— class 0 will have index 1, class 1 index 2, and so on.
targets:add(1)
— construct inputs and targets dp.Views
local input_v, target_v = dp.SequenceView(), dp.ClassView()
input_v:forward(self._text_axes, inputs)
target_v:forward(‘b’, targets)
target_v:setClasses(self._classes)
— construct dataset
dataset= dp.DataSet{inputs=input_v,targets=target_v,which_set=which_set}
–print(dataset)
return dataset
end

function Weibo:loadData(file_name)
local path="../"..file_name
print(file_name)
— backwards compatible with old binary format
local status, data = pcall(function() return torch.load(path) end)
if not status then
return torch.load(path, "binary")
end
return data
end
[/code]

  • 实验代码编写
    使用cnn的方式处理,分为三层,第一层是一个一维卷积,第二层和第三层都是传统的神经网络写法。

[code]
require ‘dp’
require ‘weiboSource’

–[[hyperparameters]]–
opt = {
nHidden = 100, –number of hidden units
learningRate = 0.1, –training learning rate
momentum = 0.9, –momentum factor to use for training
maxOutNorm = 1, –maximum norm allowed for output neuron weights
batchSize = 128, –number of examples per mini-batch
maxTries = 100, –maximum number of epochs without reduction in validation error.
maxEpoch = 1000, –maximum number of epochs of training
cuda =false,
useDevice =1,
inputEmbeddingSize =100,
outputEmbeddingSize=100,

convOutputSize=50,
convKernelSize=2,
convKernelStride=1,
convPoolSize=2,
convPoolStride=2,
contextSize=4,
decayPoint=100 ,–epoch at which learning rate is decayed
decayFactor=0.1, –‘factory by which learning rate is decayed at decay point’
}

local datasource=dp.Weibo()

inputModel = dp.Convolution1D{
input_size = opt.inputEmbeddingSize,
output_size = opt.convOutputSize,
kernel_size = opt.convKernelSize,
kernel_stride = opt.convKernelStride,
pool_size = opt.convPoolSize,
pool_stride = opt.convPoolStride,
transfer = nn.Tanh(),
dropout = opt.dropout and nn.Dropout() or nil,
acc_update = opt.accUpdate
}
local nOutputFrame = inputModel:outputSize(opt.contextSize, ‘bwc’)
dp.vprint(not opt.silent, "Convolution has "..nOutputFrame.." output Frames")
inputSize = nOutputFrame*opt.convOutputSize
–print(hiddenModel)

softmax = dp.Neural{
input_size = opt.outputEmbeddingSize,
output_size = table.length(datasource:classes()),
transfer = nn.LogSoftMax(),
dropout = opt.dropout and nn.Dropout() or nil,
acc_update = opt.accUpdate
}

mlp = dp.Sequential{
models = {
inputModel,
dp.Neural{
input_size = inputSize,
output_size = opt.outputEmbeddingSize,
transfer = nn.Tanh(),
dropout = opt.dropout and nn.Dropout() or nil,
acc_update = opt.accUpdate
}
}
}

–[[Propagators]]–
train = dp.Optimizer{
loss = opt.softmaxtree and dp.TreeNLL() or dp.NLL(),
visitor = {
dp.Learn{
learning_rate = opt.learningRate,
observer = dp.LearningRateSchedule{
schedule = {[opt.decayPoint]=opt.learningRate*opt.decayFactor}
}
},
dp.MaxNorm{max_out_norm=opt.maxOutNorm, period=opt.maxNormPeriod}
},
feedback = dp.Perplexity(),
sampler = dp.Sampler{ –shuffle sample takes too much mem
epoch_size = opt.trainEpochSize, batch_size = opt.batchSize
},
progress = opt.progress
}
valid = dp.Evaluator{
loss = opt.softmaxtree and dp.TreeNLL() or dp.NLL(),
feedback = dp.Perplexity(),
sampler = dp.Sampler{
epoch_size = opt.validEpochSize,
batch_size = opt.softmaxtree and 1024 or opt.batchSize
},
progress = opt.progress
}
tester = dp.Evaluator{
loss = opt.softmaxtree and dp.TreeNLL() or dp.NLL(),
feedback = dp.Perplexity(),
sampler = dp.Sampler{batch_size = opt.softmaxtree and 1024 or opt.batchSize}
}

–[[Experiment]]–
xp = dp.Experiment{
model = mlp,
optimizer = train,
validator = valid,
tester = tester,
observer = (not opt.trainOnly) and {
dp.FileLogger(),
dp.EarlyStopper{max_epochs = opt.maxTries}
} or nil,
random_seed = os.time(),
max_epoch = opt.maxEpoch
}
–[[GPU or CPU]]–
if opt.cuda then
require ‘cutorch’
require ‘cunn’
if opt.softmaxtree or opt.softmaxforest then
require ‘cunnx’
end
cutorch.setDevice(opt.useDevice)
xp:cuda()
end

print"dp.Models :"
print(mlp)
print"nn.Modules :"
trainset=datasource:trainSet():sub(1,32)

print(mlp:toModule(datasource:trainSet():sub(1,32)))

xp:run(datasource)
[/code]

  • 实验结果
    实验结果在测试集上5分类达到了70%+,令我感到十分意外,真是意外之喜

先上代码,说明后面再加。

[python]
#-*- coding:utf-8 -*-
”’
Created on 2014-04-28

@author: Howard
”’
from os import path, sys

sys.path.append("../")
from nlpir import seg
import numpy
import cPickle, codecs, os

class NaiveBayes:
def __init__(self):
self.segmentor = seg.Seg()

def createVocabList(self, dataSet):
"创建一个词表"
#❶ 创建一个空集
self.vocabSet = set([])
for document in dataSet:
#❷ 创建两个集合的并集
self.vocabSet = self.vocabSet | set(document)
self.vocabList = list(self.vocabSet)

”’
def addATrainDoc(self,doc):
self.vocabSet=self.vocabSet|set(doc)
self.vocabList=list(self.vocabSet)
”’

def bagOfWords2VecMN(self, inputSet):
#向量的每个位置上保存的是该位置词出现的个数,即向量各个分量之和等于文档总词数。
returnVec = [0] * len(self.vocabList)
for word in inputSet:
if word in self.vocabList:
returnVec[self.vocabList.index(word)] += 1
return returnVec

def trainNB0(self, trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix)

#保存总文档数,扩展时使用
self.documentNum = numTrainDocs

numWords = len(trainMatrix[0])
#所有垃圾的正好是1,所以正好是加起来的数除以总分类数
p1 = sum(trainCategory) / float(numTrainDocs)
#❶ (以下两行)初始化概率 ,加入平滑(加一平滑)
p0Num = numpy.ones(numWords);
p1Num = numpy.ones(numWords)
p0Denom = 0.0;
p1Denom = 0.0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
#❷(以下两行)向量相加
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])

#保存各个类型文档的数目,扩展时使用
self.p1wordNum = p1Denom
self.p0wordNum = p0Denom

p1Vect = numpy.log(p1Num / (p1Denom + len(self.vocabList))) #加一平滑的分母为 总单词数+|V|
p0Vect = numpy.log(p0Num / (p0Denom + len(self.vocabList))) #加一平滑的分母为 总单词数+|V|
#self.p1VectDict = dict(zip(self.vocabList, p1Vect)) #1类中所有单词的概率值
#self.p0VectDict = dict(zip(self.vocabList, p0Vect)) #0类中所有单词的概率值
self.vectDict = {}
for i in range(len(self.vocabList)):
self.vectDict[self.vocabList[i]] = {‘p1’: p1Vect[i], ‘p0’: p0Vect[i]}
self.p1 = p1
self.p0 = 1 – p1
return p0Vect, p1Vect, p1

def classifyNB(self, words):
"""
vec2Classify 词向量
p0Vec 在0类中的概率向量
p1Vec 在1类中的概率向量
pClass1 1类的概率
"""
#❶ 元素相乘
p1 = numpy.log(self.p1)
p0 = numpy.log(self.p0)
v1Values = [self.vectDict.get(word, {‘p1’: numpy.log(1.0 / (self.p1wordNum + len(self.vectDict)))})[‘p1’] for
word in
words] #加一平滑中未见词的概率为 总单词数+|V|
v0Values = [self.vectDict.get(word, {‘p0’: numpy.log(1.0 / (self.p0wordNum + len(self.vectDict)))})[‘p0’] for
word in words] #加一平滑中未见词的概率为 总单词数+|V|
p1 += numpy.sum(v1Values)
p0 += numpy.sum(v0Values)
if p1 > p0:
return 1, p1, p0
else:
return 0, p1, p0

def _calcMostFreq(self, fullText):
import operator

freqDict = {}
for token in self.vocabList:
freqDict[token] = fullText.count(token)
sortedFreq = sorted(freqDict.iteritems(), key=operator.itemgetter(1), reverse=True)
return sortedFreq[:30]

def saveModel(self, path):
outputfile = open(path, "w")
cPickle.dump(self.p0, outputfile)
cPickle.dump(self.p1, outputfile)
cPickle.dump(self.vectDict, outputfile)
cPickle.dump(self.documentNum, outputfile)
cPickle.dump(self.p1wordNum, outputfile)
cPickle.dump(self.p0wordNum, outputfile)
outputfile.close()

def loadModel(self, path):
if os.path.isfile(path):
inputfile = open(path, ‘r’)
self.p0 = cPickle.load(inputfile)
self.p1 = cPickle.load(inputfile)
self.vectDict = cPickle.load(inputfile)
self.documentNum = cPickle.load(inputfile)
self.p1wordNum = cPickle.load(inputfile)
self.p0wordNum = cPickle.load(inputfile)
inputfile.close()
return True
else:
return False

def increase(self, text, lable):
”’
贝叶斯增量学习
”’
words = self.segmentor.seg(text).split()
wordslength = len(words)
if lable == 1:
p1wordslength = wordslength
p0wordslength = 0
else:
p1wordslength = 0
p0wordslength = wordslength
#更新不同标记的分类的频率 步骤一
self.p0 = self.documentNum * self.p0 / (self.documentNum + 1)
self.p1 = self.documentNum * self.p1 / (self.documentNum + 1)

newwordscount = 0
for word in words:
if not self.vectDict.has_key(word):
newwordscount += 1
dictlength = len(self.vectDict)

def updateFrequency1(key):
self.vectDict[key][‘p1’] += numpy.log(
(self.p1wordNum + dictlength) / (self.p1wordNum + (dictlength + newwordscount) + p1wordslength))

def updateFrequency0(key):
self.vectDict[key][‘p0’] += numpy.log(
(self.p0wordNum + dictlength) / (self.p0wordNum + (dictlength + newwordscount) + p0wordslength))

if lable == 1:
self.p1 += 1.0 / (self.documentNum + 1) #更新不同标记的分类的频率 步骤二
#更新该分类下所有词的频率
map(updateFrequency1,filter((lambda key: not words.__contains__(key)),self.vectDict.keys()))
if newwordscount != 0: #如果新词频率不等于零,还要更新对侧分类的频率值
map(updateFrequency0, self.vectDict.keys())

#更新出现了的词的频率
for word in set(words):
if self.vectDict.has_key(word):
self.vectDict[word][‘p1’] = numpy.log(
((self.p1wordNum + dictlength) * numpy.exp(self.vectDict[word][‘p1’]) + words.count(word)) / (
self.p1wordNum + (dictlength + newwordscount) + p1wordslength))
else:
self.vectDict[word] = {}
self.vectDict[word][‘p1’] = numpy.log((words.count(word) + 1.0) / (
self.p1wordNum + (dictlength + newwordscount) + p1wordslength)) #+1是为了平滑,与最初构建时的加一平滑一致
self.vectDict[word][‘p0’] = numpy.log(
1.0 / (self.p0wordNum + (dictlength + newwordscount) + p0wordslength))
self.p1wordNum += wordslength

else:
self.p0 += 1.0 / (self.documentNum + 1) #更新不同标记的分类的频率 步骤二
#更新该分类下所有词的频率
map(updateFrequency0, filter((lambda key: not words.__contains__(key)),self.vectDict.keys()))
if newwordscount != 0: #如果新词频率不等于零,还要更新对侧分类的频率值
map(updateFrequency1, self.vectDict.keys())

#更新出现了的词的频率
for word in set(words):
if self.vectDict.has_key(word):
self.vectDict[word][‘p0’] = numpy.log(
((self.p0wordNum + dictlength) * numpy.exp(self.vectDict[word][‘p0’]) + words.count(word)) / (
self.p0wordNum + (dictlength + newwordscount) + p0wordslength))
else:
self.vectDict[word] = {}
self.vectDict[word][‘p0’] = numpy.log((words.count(word) + 1.0) / (
self.p0wordNum + (dictlength + newwordscount) + p0wordslength)) #+1是为了平滑,与最初构建时的加一平滑一致
self.vectDict[word][‘p1′] = numpy.log(
1.0 / (self.p1wordNum + (dictlength + newwordscount) + p1wordslength))
self.p0wordNum += wordslength
self.documentNum += 1

def train(self, texts, lables):
wordMatrix = []
fullText = []
for text in texts:
words = self.segmentor.seg(text).split()
wordMatrix.append(words)
fullText.extend(words)
self.createVocabList(wordMatrix)
”’
top30Words = self._calcMostFreq(fullText)
for pairW in top30Words:
if pairW[0] in self.vocabList: self.vocabList.remove(pairW[0])
”’
trainMatrix = []
for docs in wordMatrix:
trainMatrix.append(self.bagOfWords2VecMN(docs))
self.trainNB0(numpy.array(trainMatrix), numpy.array(lables))

def classify(self, text):
words = self.segmentor.seg(text).split()
return self.classifyNB(words)

if __name__ == ‘__main__’:
texts = [u’i am spam 1′, u’i am not spam 2′, u’i am spam 2′, ‘i am not spam 3′]
#texts = [u’i am spam 1′, u’i am not spam 1’, u”, ”]
lables = [1, 0, 1, 0]

bayes1 = NaiveBayes()
bayes1.train(texts, lables)
print ‘testing spam’
psum = 0
for (k, v) in bayes1.vectDict.items():
psum += numpy.exp(v[‘p1’])
print k, v[‘p1’]
print psum
print ‘testing common’
psum = 0
for (k, v) in bayes1.vectDict.items():
psum += numpy.exp(v[‘p0’])
print k, v[‘p0′]
print psum
bayes2 = NaiveBayes()
bayes2.train(texts[:2], lables[:2])
bayes2.increase(u’i am spam 2′, 1)
bayes2.increase(u’i am not spam 3’, 0)
#bayes2.increase(u”, 1)
#bayes2.increase(u”, 0)
print ‘testing spam’
psum = 0
for (k, v) in bayes2.vectDict.items():
psum += numpy.exp(v[‘p1’])
print k, v[‘p1’]
print psum
print ‘testing common’
psum = 0
for (k, v) in bayes2.vectDict.items():
psum += numpy.exp(v[‘p0’])
print k, v[‘p0’]
print psum
[/python]

参考文献:http://www.ituring.com.cn/article/32338

(1)      使用JNI对CRF++进行封装

由于CRF++是由C++语言编写的条件随机场工具,为了在系统主体的Java程序中使用,需要使用JNI技术对Java进行扩展,并将CRF++编译为可被Java加载和调用的动态链接库。

JNI是Java Native Interface的缩写,中文为JAVA本地调用,它允许Java代码和其他语言写的代码进行交互。在没有SWIG工具时,需要手工编写大量代码用于将C++代码封装为可供Java调用的接口函数,在SWIG工具的帮助下,能大量减少工作。

SWIG是个帮助使用C或者C++编写的软件能与其它各种高级编程语言进行嵌入联接的开发工具。在本系统中,我们使用SWIG将C++编写的CRF++程序与Java编写的系统主要部分联接。

下载SWIG Windows版,解压到某一安装目录下,将含有swig.exe的目录添加的环境变量Path中,供后续使用。

SWIG对程序的封装需要编写接口文件,在接口文件中导入“crfpp.h”文件,使得SWIG导出crfpp.h中定义的接口,将其封装为JNI。编写接口文件CRFPP.i如下:

[code]
%module CRFPP
%include exception.i
%{
#include "crfpp.h"
%}
%newobject surface;
%exception {
try { $action }
catch (char *e) { SWIG_exception (SWIG_RuntimeError, e); }
catch (const char *e) { SWIG_exception (SWIG_RuntimeError, (char*)e); }
}
%feature("notabstract") CRFPP::Tagger;
%ignore CRFPP::createTagger;
%ignore CRFPP::getTaggerError;
%extend CRFPP::Tagger { Tagger(const char *argc); }
%{
void delete_CRFPP_Tagger (CRFPP::Tagger *t) {
delete t;
t = 0;
}
CRFPP::Tagger* new_CRFPP_Tagger (const char *arg) {
CRFPP::Tagger *tagger = CRFPP::createTagger(arg);
if (! tagger) throw CRFPP::getTaggerError();
return tagger;
}
%}
%include crfpp.h
%include version.h
[/code]

其中crfpp.h与version.h文件都可以在CRF++源代码中找到,但需将crfpp.h替换为前文中提到的64位的头文件。

进入Windows命令行界面,切换至含有CRFPP.i接口文件的路径,输入命令:

[code]swig –c++ -java -package org.chasen.crfpp CRFPP.i[/code]

命令执行完后会生成如表生成的CRF++封装文件所示的6个文件。

生成的CRF++封装文件

CRFPP.java
CRFPPConstants.java
CRFPPJNI.java
SWIGTYPE_p_unsigned___int64.java
Tagger.java
CRFPP_wrap.cxx

其中,CRFPP_wrap.cxx文件是C++端的封装文件用于将CRF++封装为JNI接口。将其拷贝至CRF++编译工程目录。

将另外5个Java文件拷贝至本系统主体Java工程的org.chasen.crfpp包中,其中Tagger类是我们将要使用到的主要类。

(2)      JNI中文乱码的解决

由于在java内部是使用的16bit的Unicode编码(UTF-16)来表示字符串的,无论英文还是中文都是2字节;JNI内部是使用UTF -8编码来表示字符串的,UTF -8是变长编码的Unicode,一般ASCII字符是1字节,中文是3字节;C/C++使用的是原始数据,ASCII就是一个字节,在Windows操作系统中,中文一般是GB2312编码,用2个字节表示一个汉字。[21]如果直接在Java程序中传入中文字符串将会出现不可逆的中文乱码,对命名实体识别的效果有很大影响。

在swig生成的CRFPP_wrap.cxx文件中,在约270行的位置找到“#include “crfpp.h””,在这条语句下面加入两个函数:

l  JStringToWindows

[cpp]
//to use chinese never foget to free.
char * JStringToWindows(JNIEnv * pJNIEnv, jstring jstr)
{
jsize len = pJNIEnv->GetStringLength(jstr);
const jchar * jcstr = pJNIEnv->GetStringChars(jstr, NULL);
int size = 0;
char * str = (char *)malloc(len * 2 + 1);
if ((size = WideCharToMultiByte(CP_ACP, 0, LPCWSTR(jcstr), len, str, len * 2 + 1, NULL, NULL)) == 0)
return NULL;
pJNIEnv->ReleaseStringChars(jstr, jcstr);
str[size] = 0;
return str;
}
[/cpp]
l  WindowsTojstring

[cpp]
jstring WindowsTojstring( JNIEnv* env, char* str )
{
jstring rtn = 0;
int slen = strlen(str);
unsigned short * buffer = 0;
if( slen == 0 )
rtn = (env)->NewStringUTF(str );
else
{
int length = MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, NULL, 0 );
buffer = (unsigned short *)malloc( length*2 + 1 );
if( MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, (LPWSTR)buffer, length ) >0 )
rtn = (env)->NewString(  (jchar*)buffer, length );
}
if( buffer )
free( buffer );
return rtn;
}
[/cpp]

在源文件中查找“GetStringUTFChars”函数,将其替换为“JStringToWindows”,查找“NewStringUTF”,将其替换为“WindowsTojstring”。并在“JStringToWindows”函数返回的字符串使用后,使用stdio标准库中的“free”函数将其释放,以防造成内存泄露。

在CRF++工程中,以动态链接库形式编译CRFPP_wrap.cxx为CRFPP.dll文件。将该文件拷贝到本系统Java主体的工程根目录下。

(3)      JNI封装后CRF++的使用

在Tagger类中加入如下代码:

[java]
static {
try {
System.loadLibrary("CRFPP");
} catch (UnsatisfiedLinkError e) {
System.err.println("Cannot load the example native code.\nMake sure your LD_LIBRARY_PATH contains \’.\’\n"+ e);
System.exit(1);
}
}
[/java]

在Tagger类被调用的时候,会自动在环境变量的Path路径下查找CRFPP.dll动态链接库,作为CRF++的核心载入主程序中使用。

1)        初始化Tagger类时使用“-m”参数指定模型文件地址。

2)        使用boolean clear()方法清除已有的数据。

3)        使用boolean add(String str)方法添加特征,每一个add方法中添加的特征形如CRF++的训练文件,只是少了标注列。

4)        使用boolean parse()方法出发CRF++的标注。

5)        使用String y2(long i)方法获取对应序号CRF++标注。

在国内研究自然语言处理的人大概都知道ICTCLAS分词系统的大名。该系统是由张华平博士开发的基于层叠隐性马可夫链的分词系统,在中文分词领域具有领先的优势。今年一月张华平博士已经放出了最新的ICTCLAS2013分词系统,并更名为NLPIR,加入了新词发现,微博分词等功能。

在官方网站上,提供了C,Java,C#等语言的绑定。本文将介绍把最新的NLPIR分词系统与Python绑定的方法。

首先下载swig,swig可以帮助我们将C或者C++编写的DLL或者SO文件绑定到包括Python在内的多种语言。Windows下将安装包下载到一定目录下将该目录加入环境变量的path中即可使用swig(当然也可以输入完整的路径来使用swig)。可以打开命令行窗口,在里面输入swig,如果出现“Must specify an input file. Use -help for available options.”则表示一切顺利。

下面通过swig来讲NLPIR系统封装成Python的扩展。新建一个空白的目录,取名为nlpirpy_ext。首先要写的是swig的接口文件,可以参考swig文档的写法,下面就简单介绍一下:

[code]
%module NLPIR
%{
#define SWIG_FILE_WITH_INIT
#include "NLPIR.h"
%}
/*封装所有头文件中的api*/
#define POS_MAP_NUMBER 4
#define ICT_POS_MAP_FIRST 1
#define ICT_POS_MAP_SECOND 0
#define PKU_POS_MAP_SECOND 2
#define PKU_POS_MAP_FIRST 3
#define POS_SIZE 40
#define GBK_CODE 0
#define UTF8_CODE GBK_CODE+1
#define BIG5_CODE GBK_CODE+2
#define GBK_FANTI_CODE GBK_CODE+3
bool NLPIR_Init(const char * sDataPath=0,int encode=GBK_CODE);
bool NLPIR_Exit();
const char * NLPIR_ParagraphProcess(const char *sParagraph,int bPOStagged=1);
const result_t * NLPIR_ParagraphProcessA(const char *sParagraph,int *pResultCount,bool bUserDict=true);
int NLPIR_GetParagraphProcessAWordCount(const char *sParagraph);
void NLPIR_ParagraphProcessAW(int nCount,result_t * result);
double NLPIR_FileProcess(const char *sSourceFilename,const char *sResultFilename,int bPOStagged=1);
unsigned int NLPIR_ImportUserDict(const char *sFilename);
int NLPIR_AddUserWord(const char *sWord);//add by qp 2008.11.10
int NLPIR_SaveTheUsrDic();
int NLPIR_DelUsrWord(const char *sWord);
double NLPIR_GetUniProb(const char *sWord);
bool NLPIR_IsWord(const char *sWord);
const char * NLPIR_GetKeyWords(const char *sLine,int nMaxKeyLimit=50,bool bWeightOut=false);
const char * NLPIR_GetFileKeyWords(const char *sFilename,int nMaxKeyLimit=50,bool bWeightOut=false);
const char * NLPIR_GetNewWords(const char *sLine,int nMaxKeyLimit=50,bool bWeightOut=false);
const char * NLPIR_GetFileNewWords(const char *sFilename,int nMaxKeyLimit=50,bool bWeightOut=false);
unsigned long NLPIR_FingerPrint(const char *sLine);
int NLPIR_SetPOSmap(int nPOSmap);
bool NLPIR_NWI_Start();//New Word Indentification Start
int NLPIR_NWI_AddFile(const char *sFilename);
bool NLPIR_NWI_AddMem(const char *sText);
bool NLPIR_NWI_Complete();
const char * NLPIR_NWI_GetResult(bool bWeightOut=false);
unsigned int NLPIR_NWI_Result2UserDict();
[/code]

%module 后面跟的是模块名,也就是Python中import后跟名字。

%{
%}中的是放头文件的地方(所以说用swig做封装的话,至少得有头文件。)

其中“#define SWIG_FILE_WITH_INIT”是说封装成扩展的时候包含python的__init__.py的内容。”#include “NLPIR.h””大家懂的。

接下来那一堆#define也就是从头文件中复制出来的,这样在封装之后这些define的内容直接可以再模块中使用。

再接下来的函数声明就是要从C文件中导出的接口了。

下面再写一个setup.bat文件,将swig和扩展接口的工作统一起来:

[code]
swig -c++ -python NLPIR.i
python setup.py build_ext –inplace
echo "Build Complete!"
pause
[/code]

第一行就是swig的命令。-c++是说扩展的是C++编写的库, -python当然是说扩展为Python,NLPIR.i就是刚才写的接口文件

第二行我不懂……这是照别人写的,Python刚入门,对这些都还不太熟悉。包括接下来setup.py里面的内容也不太懂,都是依样画葫芦。大家可以参看以下这方面的教程。

[python]
”’
setup.py file for NLPIR
”’
from distutils.core import setup, Extension

NLPIR_module = Extension(‘_NLPIR’,sources=[‘NLPIR_wrap.cxx’], libraries = [‘NLPIR’])
setup(name = ‘NLPIR’,
version = ‘1.0’,
author = ‘SWIG Docs’,
description = ‘pyd for NLPIR’,
ext_modules = [NLPIR_module],
py_modules = [‘NLPIR’],
)
[/python]

要注意的两点是,第一是Extension的第一个参数前一定要有个”_”这是swig的规定,souces里面的cxx代表的就是c++文件。是第一步swig自动生成的文件。

接下来在目录里放置一个空白的__init__.py文件。为的是这个扩展可以直接放到python的目录中取。

最后,讲下载好的NLPIR文件夹中的NLPIR.dll,NLPIR.h,NLPIR.lib,Data文件夹放到和刚才同样的目录下。运行setup.bat

我就是在这里遇到了很大的问题的:我的是64位的win7系统,装的是64位的Python2.7.3。首先,在运行bat文件后,第一步能成功,在第二步的时候,首先Python会去查找VS2008的编译器,因为windows Python2.7.3本身就是VS2008编译的,但我恰恰装的是VS2012Express,所以查找失败。爆出:“Unable to find vcvarsall.bat”的错误,几经周折,发现如下解决方法:直接更改C:/Python32/Lib/distutils/msvc9compiler.py文件。。。然后失败了。。然后找到了改环境变量的方法:添加”VS90COMNTOOLS”指向VS2012的环境,例如C:\Program Files\Microsoft Visual Studio 10.0\Common7\Tools\。

到此时好像更进一步了,但是又遇到了新的问题:不是正确的win32文件。然后我发现问题出在Express版本的编译器不能编译64位文件……要么安装一个1.4G的windows的东西,要么换成32位版本的python……我表示败了,还是换32版本python吧,两个版本并存好了。

重新安装python32位版,更改环境变量指向新的Python,搞定。

上述工作完成后,将文件夹拷到python安装目录下的lib\site-packages目录下,就可以直接使用该扩展了。

下面是一个文中所提到的源代码的打包下载,一个是只含上述源代码的:nlpirpy_ext原始版.7z。另一个已经通过swig封装过了,目前只支持windows python2.7.3 32位版。其他环境需要自己封装或者测试,还带有一个小的Seg.py,是进一步封装的分词器类:NLPIRPY_EXT_built版.7z

本文就到这里了,欢迎指正:

参考文献:

一、现有命名实体识别使用方法及达到的效果

现有的命名实体识别方法,主要包括隐性马科夫链,最大熵,CRF等。

根据综述:在这4种学习方法中,最大熵模型结构紧凑,具有较好的通用性,主要缺点是训练时间复杂性非常高,有时甚至导致训练代价难以承受,另外由于需要明确的归一化计算,导致开销比较大。而条件随机场为命名实体识别提供了一个特征灵活、全局最优的标注框架,但同时存在收敛速度慢、训练时间长的问题。一般说来,最大熵和支持向量机在正确率上要比隐马尔可夫模型高一些,但是隐马尔可夫模型在训练和识别时的速度要快一些,主要是由于在利用Viterbi算法求解命名实体类别序列的效率较高。隐马尔可夫模型更适用于一些对实时性有要求以及像信息检索这样需要处理大量文本的应用,如短文本命名实体识别。

但在实际实现中,仍然有不少采用ME的实现实例。但是可能是由于所找论文的质量原因,由ME实现的命名实体识别效果并不理想。反而是几个基于隐性马科夫链的实现效果较好。目前找到的唯一的基于CRF实现的实例效果并不是很好。

现将现有的一些实现及其效果罗列如下:

基于ME的方法:

1.南京大学2007硕士学位论文:吴宝琪《中文命名实体的识别方法研究及其实现》,对于人名识别:准确率为68.O%,召回率为40.2%,F值为50.5%对于地名识别:准确率为48.7%,召回率为31.1%,F值为39.O%对于组织名识别:准确率为51.2%,召回率为27.6%,F值为35.9%。其效果不好的原因由几个,一个是特征选择有待考量,二个是为了研究而研究,没有使用分词技术。但是论文中有一些可取的筛选方法。

2.哈工大2009年:付瑞吉,车万翔,刘挺《一种基于分类方法的音乐命名实体识别技术》音乐命名实体识别总的精确率、召回率和F值分别达到了89.89%,81.01%,87.93%。

3. 余传明,黄建秋,郭 飞。2011《从客户评论中识别命名实体》

从客户评论中识别命名实体

从客户评论中识别命名实体

中采用的几个模板,效果都不好。同样存在训练集太小的因素。

基于CRF的方法:

1.苏州大学2010年学位论文:史海峰《基于CRF的中文命名实体识别研究》。

命名实体 精确率P 召回率R F值
人名 98.4% 65.7% 78.8%
地名 96.3% 67.2% 79.2%
机构名 98.3% 78.2% 87.1%

作者分析可能产生了过拟合。本论文使用的训练集过小。

基于HMM的方法:

1.2004年哈工大:廖先桃 于海滨 秦兵 刘挺《HMM与自动规则提取相结合的中文命名实体识别》。

HMM与自动规则提取相结合的中文命名实体识别

HMM与自动规则提取相结合的中文命名实体识别

 

2.2006年中科院:俞鸿魁,张华平,刘群,吕学强,施水才《基于层叠隐马尔可夫模型的中文命名实体识别》。

封闭测试:

基于层叠隐马尔可夫模型的中文命名实体识别 封闭测试

基于层叠隐马尔可夫模型的中文命名实体识别 封闭测试

开放测试(人民日报1998.6):

基于层叠隐马尔可夫模型的中文命名实体识别 开放测试

基于层叠隐马尔可夫模型的中文命名实体识别 开放测试

总结:

最大熵方法在理论上有最好的效果,从CoNLL.2003会议的报告来看,最大熵模型相对更适合于处理命名实体识别问题,在参赛的16对选手中,有5对选手使用了最大熵模型。而且从竞赛的结果来看,英文的命名实体识别竞赛的前三名和德语的命名实体识别竞赛的前两名都采用了最大熵的方法,但是训练成本比较高。在CRF方面的研究还比较少。HMM模型看来能够取得的效果也不差,可以再HMM上加以进一步改进以提高其效果。