线程安全的SRILM语言模型c++接口

博客地址:http://blog.csdn.net/wangxinginnlp/article/details/46963659

老版本线程不安全

最近几天,在倒腾多线程的翻译解码器。单线程没问题的解码器,放在多线程下就经常无故的 segmentation fault (core dumped)。排查了一天原因,才发现是语言模型的问题。

老版本的SRILM不支持多线程,多个进程环境下报错。错误具体表现如下:

  1. 将语言模型作为公共资源,多个线程去读取,会报segmentation fault (core dumped)。
  2. 将语言模型作为线程资源,多个线程各自去读取然后使用。发现只有第一个线程能够成功加载语言模型资源,其他语言模型加载语言资源失败。程序不会报错,但是翻译结果中所有的语言模型得分为0。
  3. 将语言模型作为线程资源,进程先准备多个语言模型资源(就是new多个对象)。然后分发给各个线程,供线程使用。这个时候会报错segmentation fault (core dumped)。

总而言之,老版本的SRILM在多线程下无法成功使用。

判断自己的SRILM是否是老版本,查看自己的SRILM接口。如果读取资源和给word进行条件概率打分分别为

  1. void *sriLoadLM(const char *fn, int arpa = 0, int order = 3, int unk = 0, int tolow=0);
  2. double sriWordProb(void *plm, const char *word, const char *context);

恭喜你,你的SRILM是老版本的。

新版本线程安全

现在问题是,怎么确定新版本线程安全的?

现在去SRILM官网(http://www.speech.sri.com/projects/srilm/)下载的新版本,解压压缩包后在根目录的doc目录下有一个README-THREADS。第一段是这么描述的

As of November, 2012 SRILM supports multi-threaded applications. This enhancment applies to the five libraries thatcomprise SRILM: libmisc, libdstruct,
liboolm, libflm and liblattice.  Please note that this does not imply that all APImethods arethread-safe, but rather that it is possible to perform independent SRILM tasks on
multiple threadswithout interference or instability. Some APIs that perform read-only calculations may be safe to call on objectsshared by multiple threads but in general this is not
safe, particularly on APIs that mutate data structures notsolely owned by the current thread.
We will attempt to document specific allowances and limitations within this READMEand
inline in the code.

黑体字是重点,简单说就是新版本SRILM是读安全,写不一定安全。

但是比较坑是谷歌“SRILM接口” “SRILM API” 等都无法得到官方的接口(查到的都是老版本的接口),唯一的例外就是(http://blog.csdn.net/mouxiaofeng/article/details/5144750)。

后来发现在根目录下的doc目录下有lm-intro文件。有如下这么一段话

API FOR LANGUAGE MODELS

These programs are just examples of how to use the object-oriented language model library currently under construction.  To  use the API one would have to read the various
.h files and how the interfaces are used in the example progams.  Nocomprehensive documentation is available as yet.  Sorry.

黑体字是重点,简单说就是官方不提供接口。

万幸的是,在Github上找到一个人写的python和perl接口:见https://github.com/desilinguist/swig-srilm(后称 python版接口)。但是这个版本给出的计算概率接口都是n-gram的概率:getUnigramProb,getBigramProb,getTrigramProb,getNgramProb或者是算句子概率的getSentenceProb。而在我们解码器中,需要是在给定语言模型后,给定word,计算contex条件下的概率。下面我们就在上python版接口的基础上改一个C++的接口。

查看python版接口的srilm.c文件,很容易知道其语言模型是一个Ngram类型。查看python版接口incldue中Ngram.h,发现Ngram公共继承LM,并且他有一个wordProb(VocabIndex word, const VocabIndex *context)接口。大喜,这个接口就是我们需要的。

如果好奇LM是什么,查看python版接口incldue中LM.h文件。

第一段是官方的文档

LM.h --

* Generic LM interface

* The LM class defines an abstract languge model interface which all other classes refine and inherit from.

继续看,会发现他也有 LogP wordProb(VocabIndex word, const VocabIndex *context) = 0,而且他还有LogP wordProb(VocabString word, const VocabString *context)接口。

直觉上VocabIndex是word的编号,VocabString就是string类的word。猜测不全对。查看Vocab.h,有如下

我们所追求的就是LM中wordProb(VocabString word, const VocabString *context)。如果现在经受不住考验,直接python版接口中srilm去做一个LM类包装,你会发现LM的read()函数报错,因为它根本没有实现。查看解压后SRILM目录中lm/src/LM.cc文件。

虽然他实现了wordProb(VocabString word, const VocabString *context)。

虽然他有一个可能的写操作,但是addUnkWords函数默认是flase

其实这个接口用起来没问题,个人一直不大会倒腾wordProb(VocabString word, const VocabString *context)第二个参数中那个多维数组char**。

我自己的解决方案是想办法把Ngram的WordProb合理利用起来。查看srilm.c中计算n-gram概率,无非就是先把n切分,然后去vocab中查每个word的index,最后送去计算。

<span style="font-size:18px;">// get generic n-gram probability (up to n=7)
 float getNgramProb(Ngram* ngram, const char* ngramstr, unsigned order) {
     const char* words[7];
     unsigned int indices[order];
     int numparsed, histsize, i, j;
     char* scp;
     float ans;

     // Duplicate string so that we don't mess up the original
     scp = strdupa(ngramstr);

     // Parse the given string into words
     numparsed = Vocab::parseWords(scp, (VocabString *)words, 7);                                      //切分
     if(numparsed != order) {
         fprintf(stderr, "Error: Given order (%d) does not match number of words (%d).\n", order, numparsed);
         return 0;
     }

     // Get indices for the words obtained above, if you don't find them, then add them
     // to the vocabulary and then get the indices.
     swig_srilm_vocab->addWords((VocabString *)words, (VocabIndex *)indices, order);                  //查word的index(此处写操作,线程不安全)

     // Create a history array of size "order" and populate it                                        //计算概率
     unsigned hist[order];
     for(i=order; i>1; i--) {
         hist[order-i] = indices[i-2];
     }
     hist[order-1] = Vocab_None;

     // Compute the ngram probability
     ans = getWordProb(ngram, indices[order-1], hist);

     // Return the representation of log(0) if needed
     if(ans == LogP_Zero)
        return BIGNEG;

    return ans;
}</span>

上面指出Vocab类addWords有写操作,线程不安全,推荐使用getIndices,只有读操作,线程安全。

Vocab.h中

Vocab.cc中

addWords在找不到word时候会将word写入vocab,貌似getIndices不会有(其函数实现中在有条件下也有addWord操作,等后面看清楚再确认下。实验表明是线程安全的。)。

编译接口

  1. 建一个SRILM c++接口目录SRILM_interface。
  2. 准备include和lib资源。根据python版接口的介绍,编译接口需要准备相关的静态库和头文件。直接将编译通过的SRILM工具中的根目录include目录和lib目录拷贝至目录SRILM_interface下。或者在编译时候指定路径也行。
  3. 将我们改写的srilm.h和srilm.cc,和main.cc放在SRILM_interface中。
  4. 编译:g++ srilm.h srilm.cc main.cc -I ./include ./lib/liboolm.a ./lib/libdstruct.a ./lib/libmisc.a ./lib/liblattice.a ./lib/libflm.a -lpthread

如果通过,自己在运行编译结果测试下。如果测试结果正确,c++接口就OK了。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-01 02:21:28

线程安全的SRILM语言模型c++接口的相关文章

详解~实现Runnable方法创建线程之为什么要将Runnable接口的子类对象传递给Thread的构造函数

/** * @author zhao * @TIME 0419 22:56  End *定义线程的第二种方法:实现Runnable接口 *步骤:1,定义一个子类实现Runnable接口 *    2,在子类中覆盖run()方法,并且将多线程锁执行的代码写入run方法中 *    3,通过Thread类建立线程对象: *    4,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数.  为什么要将Runnable接口的子类对象传递给Thread的构造函数.  因为,自定义的

java多线程 -- 创建线程的第三者方式 实现Callable接口

Java 5.0 在 java.util.concurrent 提供了一个新的创建执行线程的方式:Callable 接口Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的.但是 Runnable 不会返回结果,并且无法抛出经过检查的异常.Callable 需要依赖FutureTask ,FutureTask 也可以用作闭锁. 例子: package com.company; import java.util.concurrent.Callable;

从线程中产生返回值--Callable接口

Runnable是执行工作的独立线程,但是它不返回任何值.如果你希望线程在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口.在Java SE5中引入的Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call()(而不是run())中返回的值,并且必须使用ExecutorService.submit()方法调用它. 线程代码: public class ResulttThread implements Callable<String> { pr

线程池的应用及Callable接口的使用

Java代码   public interface Executor { /** * Executes the given command at some time in the future.  The command * may execute in a new thread, in a pooled thread, or in the calling * thread, at the discretion of the <tt>Executor</tt> implementa

几种创建线程方式Thread类和Runnable接口

对于很多想学习java的人来说,经常听别人说线程难,其实真正理解了线程后,一点都不会觉得线程难,这里我为大家梳理下线程的创建方式吧. 一.线程的创建方式有三种 1.继承Thread类 2.实现Runnable接口 3.实现Callable接口(返回结果并且可能抛出异常的任务). 如果采用实现Callable接口接口的方式,返回结果并且可能抛出异常的任务,不利于开发,这里就不给大家介绍了.这里咋们主要说说采用继承Thread类和实现Runnable接口的方式来创建线程. 1.继承Thread类的方

SRILM语言模型格式解读

先看一下语言模型的输出格式 [html] view plain copy \data\ ngram 1=64000 ngram 2=522530 ngram 3=173445 \1-grams: -5.24036        'cause  -0.2084827 -4.675221       'em     -0.221857 -4.989297       'n      -0.05809768 -5.365303       'til    -0.1855581 -2.111539   

实现接口创建线程

一.理论 1.进程与线程 几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程. 当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程. 2. 进程 几乎所有操作系统都支持进程的概念,所有运行中的任务通常对应一条进程(Process).当一个程序进入内存运行,即变成一个进程.进程是处于运行过程中的程序,并且具有一定独立功能, 进程是系统进行资源分配和调度的一个独立单位. 一般而言,进程包含如下三个特征: <1>独立性:进程是系统

Java线程演示样例 - 继承Thread类和实现Runnable接口

进程(Process)和线程(Thread)是程序执行的两个基本单元. Java并发编程很多其它的是和线程相关. 进程 进程是一个独立的执行单元,可将其视为一个程序或应用.然而,一个程序内部同事还包括多个进程. Java执行时环境就是一个单独的进程,在它内部还包括了作为进程的各种类和程序. 线程 能够将线程看做轻量级的进程. 线程存在于进程其中,须要的资源开销较小.同一进程中的线程共享进程的资源. Java多线程 每个Java引用都仅仅要有一个线程 - 主线程(main thread).尽管后台

JAVA学习笔记(三十八)- 创建实现Runnable接口的线程

创建实现Runnable接口的线程 /* * 创建线程方式二:实现Runnable接口 * 步骤: * 1.创建一个Runnable接口的实现类 * 2.实现run方法 * 3.创建一个实现类的实例 * 4.创建Thread实例,将实现类的实例作为参数传入 * 5.调用start方法,启动线程并运行run方法 */ class MyDemo implements Runnable{ @Override public void run() { for (int i = 1; i <= 50; i+