T2TTrainer.cpp 18.7 KB
Newer Older
xiaotong committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
/* NiuTrans.Tensor - an open-source tensor library
 * Copyright (C) 2018, Natural Language Processing Lab, Northestern University. 
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * $Created by: XIAO Tong (xiaotong@mail.neu.edu.cn) 2018-08-02
 */

xiaotong committed
22
#include <math.h>
xiaotong committed
23 24
#include "T2TTrainer.h"
#include "T2TUtility.h"
xiaotong committed
25 26
#include "../../tensor/XUtility.h"
#include "../../tensor/core/CHeader.h"
27
#include "../../tensor/loss/LHeader.h"
28
#include "../../network/XNoder.h"
xiaotong committed
29

30 31 32 33 34
#ifndef WIN32
#include <sys/time.h>
#include <unistd.h>
#endif

xiaotong committed
35 36 37 38 39 40
namespace transformer
{

/* constructor */
T2TTrainer::T2TTrainer()
{
41 42
    argNum = 0;
    argArray = NULL;
xiaotong committed
43 44 45 46 47
}

/* de-constructor */
T2TTrainer::~T2TTrainer()
{
48 49 50 51 52 53 54 55 56
    for(int i = 0; i < moments.count; i++){
        XTensor * m = (XTensor*)moments.Get(i);
        delete m;
    }

    for(int i = 0; i < moments2nd.count; i++){
        XTensor * m = (XTensor*)moments2nd.Get(i);
        delete m;
    }
57 58 59 60 61

    for(int i = 0; i < argNum; i++)
        delete[] argArray[i];
    delete[] argArray;

xiaotong committed
62 63 64 65 66 67 68
}

/* 
initialization 
>> argc - number of arguments
>> argv - list of pointers to the arguments
*/
69
void T2TTrainer::Init(int argc, char ** argv)
xiaotong committed
70
{
71 72 73 74 75 76 77
    argNum = argc;
    argArray = new char*[argc];
    for(int i = 0; i < argNum; i++){
        argArray[i] = new char[strlen(argv[i]) + 1];
        strcpy(argArray[i], argv[i]);
    }

78 79 80 81 82
    bool useMem = false;

    LoadParamBool(argc, argv, "mem", &useMem, useMem);
    LoadParamFloat(argc, argv, "lrate", &lrate, 1.0F);
    LoadParamFloat(argc, argv, "lrbias", &lrbias, 0);
xiaotong committed
83 84 85 86
    LoadParamInt(argc, argv, "sbatch", &sBatchSize, 1);
    LoadParamInt(argc, argv, "wbatch", &wBatchSize, 1);
    LoadParamInt(argc, argv, "nepoch", &nepoch, 1);
    LoadParamInt(argc, argv, "nstep", &nstep, 1);
87 88
    LoadParamInt(argc, argv, "d", &d, 512);
    LoadParamInt(argc, argv, "nwarmup", &nwarmup, 4000);
89
    LoadParamInt(argc, argv, "vsize", &vSize, 1);
90
    LoadParamInt(argc, argv, "vsizetgt", &vSizeTgt, vSize);
91 92
    LoadParamBool(argc, argv, "adam", &useAdam, false);
    LoadParamFloat(argc, argv, "adambeta1", &adamBeta1, 0.9F);
93 94 95 96 97 98 99
    LoadParamFloat(argc, argv, "adambeta2", &adamBeta2, 0.98F);
    LoadParamFloat(argc, argv, "adamdelta", &adamDelta, 1e-9F);
    LoadParamBool(argc, argv, "shuffled", &isShuffled, false);
    LoadParamFloat(argc, argv, "labelsmoothing", &labelSmoothingP, 0);
    LoadParamInt(argc, argv, "nstepcheckpoint", &nStepCheckpoint, -1);
    LoadParamBool(argc, argv, "epochcheckpoint", &useEpochCheckpoint, false);
    LoadParamInt(argc, argv, "updatestep", &updateStep, 1);
100 101
    LoadParamBool(argc, argv, "debug", &isDebugged, false);
    LoadParamBool(argc, argv, "sorted", &isLenSorted, false);
102 103 104

    adamBeta1T = 1.0F;
    adamBeta2T = 1.0F;
105 106

    batchLoader.Init(argc, argv);
xiaotong committed
107 108
}

109 110
int tc = 0;

xiaotong committed
111 112 113
/* 
train the model
>> fn - training data file
114 115
>> validFN - validation data file
>> modelFN - where we keep the model
xiaotong committed
116 117
>> model - model to train
*/
118
void T2TTrainer::Train(const char * fn, const char * validFN, const char * modelFN, T2TModel * model)
xiaotong committed
119
{
xiaotong committed
120 121
    int step = 0;
    int wc = 0;
122
    int ws =0;
xiaotong committed
123 124
    int wordCount = 0;
    int wordCountTotal = 0;
125
    int wordCountBatch = 0;
xiaotong committed
126 127
    bool isEnd = false;
    float loss = 0;
128
    float lr = 0;
129 130 131 132
    int nStepCheck = 0;
    int nCheckpoint = 0;
    int nSkipped = 0;
    int gradStep = 0;
133 134
    int validStep = 0;
    int epoch = 0;
135 136 137 138 139 140 141 142

    char * trainFN = new char[(int)strlen(fn) + 10];
    strcpy(trainFN, fn);

#ifndef WIN32
    if(isShuffled)
        sprintf(trainFN, "%s.random", fn);
#endif
143

144 145
    int devID = model->devID;
    XMem * mem = model->mem;
146
    XNet net;
xuchen committed
147

148 149
    if(isDebugged)
        net.SetGradEfficientFlag(false);
xiaotong committed
150
    
151 152
    PrepareModel(model);

xiaotong committed
153 154
    double startT = GetClockSec();
    
155
    for(epoch = 1; epoch <= nepoch; epoch++){
156
#ifndef WIN32
157
        if(isShuffled)
158
            batchLoader.Shuffle(fn, trainFN);
159
#endif
xiaotong committed
160
        
161
        FILE * file = fopen(trainFN, "rb");
xiaotong committed
162 163 164
        CheckNTErrors(file, "cannot open training file!");
        
        wordCount = 0;
165
        loss = 0;
xiaotong committed
166
        
167 168 169
        /* batch of sequences (on the encoder and decoder sides) */
        XTensor batchEnc;
        XTensor batchDec;
170

171 172 173
        /* labels */
        XTensor label;

174
        /* padding */
175 176
        XTensor paddingEnc;
        XTensor paddingDec;
177 178 179

        /* gold standard */
        XTensor gold;
xiaotong committed
180
        
181 182 183
        /* label smoothed gold standard (if needed) */
        XTensor goldSmoothed;
        
184 185 186 187
        while (batchLoader.LoadBatch(file, model->isLM, 
                                     &batchEnc, &paddingEnc, &batchDec, &paddingDec, &gold, &label,
                                     NULL, vSize, vSizeTgt,
                                     sBatchSize, wBatchSize, isLenSorted, ws, wc, devID, mem, true)) 
188
        {
189

190
            CheckNTErrors(batchEnc.order == 2, "wrong tensor order of the sequence batch");
191

xiaotong committed
192 193
            /* output probabilities */
            XTensor output;
194

xiaotong committed
195
            /* make the network */
196 197 198
            if(model->isLM)
                model->MakeLM(batchEnc, output, paddingEnc, true);
            else if(model->isMT)
xuchen committed
199
                model->MakeMT(batchEnc, batchDec, output, paddingEnc, paddingDec, true);
200 201 202
            else{
                ShowNTErrors("Illegal model type!");
            }
203

204
            /* back-propagation for obtaining gradients */
205 206
            //if (labelSmoothingP > 0)
            //    LabelSmooth(&gold, &goldSmoothed, labelSmoothingP);
207

208 209 210 211
            XTensor labelOnehot;

            labelOnehot = IndexToOnehot(label, vSizeTgt, labelSmoothingP);
            
212
            /* make paddings for the output */
213 214
            //if (output.GetDim(0) > 0)
                //PadOutput(&output, &labelOnehot, &paddingDec);
215

xiaotong committed
216
            /* get probabilities */
217 218 219 220
            //float prob = GetProb(&output, &labelOnehot, NULL);
            XTensor lossTensor;
            lossTensor = CrossEntropy(output, labelOnehot, paddingDec);
            float prob = ReduceSumAll(lossTensor);
221

222 223 224 225
            DTYPE lossLocal = prob / wc;
            bool doUpdate = (!IsNAN(lossLocal) && !IsINF(lossLocal) && lossLocal < 1e3F);
          
            //XTensor &g = labelSmoothingP > 0 ? goldSmoothed : gold;  
226 227 228 229

            if (doUpdate) {
                
                /* recale the output for normalized loss */
230
                //RescaleOutput(&output, &labelOnehot, &paddingDec);
231 232
                
                /* back-propagation */
233 234 235
                net.Backward(lossTensor);
                //net.Backward(output, labelOnehot, paddingDec, CROSSENTROPY);
                //net.Backward(output, label, labelSmoothingP, CROSSENTROPY);
236
                
237
                gradStep += 1;
238
                loss += prob;
239 240 241
                wordCount += wc;
                wordCountTotal += wc;
                
242 243
                //totalW = wc + ws;
                wordCountBatch += ws;
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
                /* update the parameters */
                if(gradStep == updateStep){
                    
                    /* learning rate */
                    lr = lrate * (1.0F / (float)sqrt((float)d)) * (float)MIN(pow((float)validStep + 1, -0.5F - lrbias), ((float)validStep + 1) * pow((float)nwarmup, -1.5F - lrbias));
                    
                    /* model update */
                    Update(model, lr);
                    
                    gradStep = 0;
                    validStep++;
                }
            }
            else
                nSkipped++;
xiaotong committed
259 260 261 262 263 264
            
            if(++step >= nstep){
                isEnd = true;
                break;
            }
            
265
            if (step % 100 == 0) {
xiaotong committed
266
                double elapsed = GetClockSec() - startT;
267 268
                XPRINT8(0, stderr, "[INFO] elapsed=%.1fs, step=%d, epoch=%d, tword=%d, sword=%d, loss=%.3f, ppl=%.3f, sppl=%.3f",
                        elapsed, step, epoch, wordCountTotal, wordCountBatch, loss/wordCount, exp(loss/wordCount), exp(prob/wc));
269 270 271
                if (!doUpdate)
                    XPRINT(0, stderr, " (no update)");
                XPRINT(0, stderr, "\n");
xiaotong committed
272
            }
273

274 275 276 277 278
            if(nStepCheckpoint > 0 && ++nStepCheck >= nStepCheckpoint){
                MakeCheckpoint(model, validFN, modelFN, "step", step);
                nStepCheck = 0;
                nCheckpoint++;
            }
xiaotong committed
279 280 281
        }
        
        fclose(file);
282
        
283
        if (isEnd)
284 285 286 287 288 289 290 291 292 293 294 295 296 297
            break;

        if(useEpochCheckpoint)
            MakeCheckpoint(model, validFN, modelFN, "epoch", epoch);
    }

    double elapsed = GetClockSec() - startT;
    
    epoch = MIN(epoch, nepoch);
    
    XPRINT7(0, stderr, "[INFO] lr=%.2e, elapsed=%.1fs, step=%d, epoch=%d, word=%d, loss=%.3f, ppl=%.3f\n",
            lr, elapsed, step, epoch, wordCountTotal, loss/wordCount, exp(loss/wordCount));
    XPRINT4(0, stderr, "[INFO] training finished (took %.1fs, step=%d, skipped=%d and epoch=%d)\n",
            elapsed, step, nSkipped, epoch);
298 299

    delete[] trainFN;
xiaotong committed
300 301
}

302 303 304 305 306 307 308 309 310
/* 
test the model
>> fn - test data file
>> ofn - output data file
>> model - model that is trained
*/
void T2TTrainer::Test(const char * fn, const char * ofn, T2TModel * model)
{
    int wc = 0;
311
    int ws = 0;
312 313
    int wordCount = 0;
    int wordCountTotal = 0;
314
    int sentCount = 0;
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
    float loss = 0;

    /* data files */
    FILE * file = fopen(fn, "rb");
    CheckNTErrors(file, "Cannot read the test file");
    FILE * ofile = fopen(ofn, "wb");
    CheckNTErrors(ofile, "Cannot open the output file");

    int devID = model->devID;
    XMem * mem = model->mem;

    XNet net;
    
    double startT = GetClockSec();
        
    wordCount = 0;
        
    /* batch of input sequences */
333 334
    XTensor batchEnc;
    XTensor batchDec;
335

336 337 338
    /* label */
    XTensor label;

339
    /* padding */
340 341
    XTensor paddingEnc;
    XTensor paddingDec;
342 343 344 345 346 347 348

    /* gold standard */
    XTensor gold;

    /* an array that keeps the sequences */
    int * seqs = new int[MILLION];
    
349
    batchLoader.ClearBuf();
350

351 352 353 354
    while(batchLoader.LoadBatch(file, model->isLM, 
                                &batchEnc, &paddingEnc, &batchDec, &paddingDec, &gold, &label,
                                seqs, vSize, vSizeTgt,
                                1, 1, false, ws, wc, devID, mem, false))
355 356
    {
        CheckNTErrors(batchEnc.order == 2, "wrong tensor order of the sequence batch");
357 358 359 360 361
            
        /* output probabilities */
        XTensor output;
            
        /* make the network */
362 363 364
        if(model->isLM)
            model->MakeLM(batchEnc, output, paddingEnc, false);
        else if(model->isMT)
xuchen committed
365
            model->MakeMT(batchEnc, batchDec, output, paddingEnc, paddingDec, false);
366 367 368
        else{
            ShowNTErrors("Illegal model type!");
        }
369

370 371
        int bSize = output.GetDim(0);
        int length = output.GetDim(1);
372 373 374 375 376

        /* prediction probabilities */
        XTensor probs;
        InitTensor1D(&probs, bSize * length);

377 378 379 380
        XTensor labelOnehot;

        labelOnehot = IndexToOnehot(label, vSizeTgt, 0);

381
        /* get probabilities */
382
        float prob = GetProb(&output, &labelOnehot, &probs);
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410

        /* dump the test result */
        for(int s = 0; s < bSize; s++){
            DTYPE sum = 0;
            int * seq = seqs + s * length;
            for(int i = 0; i < length; i++){
                if(seq[i] >= 0){
                    fprintf(ofile, "%d ", seq[i]);
                }
                else
                    break;
            }
            fprintf(ofile, "||| ");
            for(int i = 0; i < length; i++){
                if(seq[i] >= 0){
                    DTYPE p = probs.Get1D(s * length + i);
                    fprintf(ofile, "%.3e ", p);
                    sum += p;
                }
                else
                    break;
            }
            fprintf(ofile, "||| %e\n", sum);
        }
            
        loss += -prob;
        wordCount += wc;
        wordCountTotal += wc;
411
        sentCount += 1;
412 413 414 415 416 417 418 419
    }
        
    fclose(file);
    fclose(ofile);

    delete[] seqs;
    
    double elapsed = GetClockSec() - startT;
420

421 422 423 424
    XPRINT3(0, stderr, "[INFO] test finished (took %.1fs, word=%d, and ppl=%.3f)\n",
            elapsed,wordCountTotal, exp(loss / wordCount));
}

425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
/* 
make a checkpoint 
>> model - the model
>> validFN - validation data file
>> modelFN - model data file
>> label - label of the model
>> id - id of the checkpoint
*/
void T2TTrainer::MakeCheckpoint(T2TModel * model, const char * validFN, const char * modelFN, const char * label, int id)
{
    char * fn = new char[MAX_LINE_LENGTH];
    char * fn2 = new char[MAX_LINE_LENGTH];
    sprintf(fn, "%s.%s.%03d", modelFN, label, id);
    sprintf(fn2, "%s.%s.%03d.output", modelFN, label, id);

    model->Dump(fn);
441 442 443 444 445
    //if(validFN != NULL){
        //T2TTrainer trainer;
        //trainer.Init(argNum, argArray);
        //trainer.Test(validFN, fn2, model);
    //}
446 447 448 449 450

    delete[] fn;
    delete[] fn2;
}

xiaotong committed
451 452 453 454 455 456 457 458 459 460 461
/*
get word probabilities for a batch of sequences
>> output - word distribution for each position
>> gold - gold standard
>> wordProbs - word probability for gold prediction
*/
float T2TTrainer::GetProb(XTensor * output, XTensor * gold, XTensor * wordProbs)
{
    XTensor probs;
    InitTensor(&probs, output);
    
xuchen committed
462
    _Multiply(output, gold, &probs);
xiaotong committed
463 464 465 466 467 468 469 470 471 472 473 474 475
    
    /* probability of each word */
    XTensor wprobs;
    InitTensor1D(&wprobs, output->unitNum/output->GetDim(-1), X_FLOAT, output->devID, output->mem);
    
    int dims[2] = {output->unitNum/output->GetDim(-1), output->GetDim(-1)};
    probs.Reshape(2, dims);
    _ReduceSum(&probs, &wprobs, 1);
    
    if(wordProbs != NULL)
        _CopyValues(&wprobs, wordProbs);
    
    /* reshape the tensor to fit it into the reduce procedure
476
       TODO: XTensor supports scalars */
xiaotong committed
477 478 479 480 481 482 483 484 485 486 487
    dims[0] = 1;
    dims[1] = probs.unitNum;
    probs.Reshape(2, dims);
    
    /* probability for the batch */
    XTensor result;
    InitTensor1D(&result, 1, X_FLOAT, output->devID, output->mem);
    _ReduceSum(&probs, &result, 1);
    
    return result.Get1D(0);
}
xiaotong committed
488

xiaotong committed
489
/* 
490 491 492 493
update the model by delta rule
\theta_new = \theta - \lrate * grad
where
\lrate = d^-0.5 * min(stepNum^-0.5, stepNum * warmupStepNum^-1.5)
xiaotong committed
494
>> model - the t2t model
495
>> lr - learning rate
xiaotong committed
496
*/
497
void T2TTrainer::Update(T2TModel * model, const float lr)
xiaotong committed
498
{
499
    TensorList ws(100);
xiaotong committed
500

501
    model->GetParams(ws);
xiaotong committed
502 503 504 505 506

    for(int i = 0; i < ws.count; i++){
        XTensor * para = (XTensor*)ws.Get(i);
        XTensor * paraGrad = para->grad;

xiaotong committed
507
        if (paraGrad == NULL)
508 509
            continue;

xiaotong committed
510 511 512
        CheckNTErrors(para != NULL, "NULL parameter tensor!");
        CheckNTErrors(paraGrad != NULL, "NULL gradient tensor!");

513 514 515 516 517
        if(useAdam){
            adamBeta1T *= adamBeta1;
            adamBeta2T *= adamBeta2;
            DTYPE e = lr * (DTYPE)sqrt(1 - adamBeta2T) / (1 - adamBeta1T);
            DTYPE d = adamDelta * (DTYPE)sqrt(1 - adamBeta2T);
518

519
            /* m = beta_1 * m + (1-beta_1) * grad */
520 521 522 523
            XTensor * m = (XTensor*)moments.Get(i);
            _ScaleAndShiftMe(m, adamBeta1, 0);
            _Sum(m, paraGrad, m, (1.0F - adamBeta1));
            
524
            /* v = beta_2 * v + (1-beta_2) * grad * grad*/
525 526 527
            XTensor * v = (XTensor*)moments2nd.Get(i);
            _Multiply(paraGrad, paraGrad, v, adamBeta2/(1.0F - adamBeta2));
            _ScaleAndShiftMe(v, (1.0F - adamBeta2), 0);
528

529 530 531 532 533 534 535 536
            /* v2 = m / (sqrt(v) + delta) */
            XTensor * v2 = NewTensorBuf(v, v->devID, v->mem);
            _Power(v, v2, 0.5F);
            _ScaleAndShiftMe(v2, 1.0F, d);
            _Div(m, v2, v2);

            /* the delta rule */
            _Sum(para, v2, para, -e);
537

538
            DelTensorBuf(v2);
539

540 541 542 543 544
        }
        else{
            /* the delta rule */
            _Sum(para, paraGrad, para, -lr);
        }
545 546 547

        /* clear gradient */
        paraGrad->SetZeroAll();
xiaotong committed
548 549 550
    }
}

551 552 553 554 555 556 557 558 559
/* 
prepare model for training 
>> model - the model for training
*/
void T2TTrainer::PrepareModel(T2TModel * model)
{
    moments.Clear();
    moments2nd.Clear();

560
    TensorList ws(100);
561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584

    model->GetParams(ws);

    for(int i = 0; i < ws.count; i++){
        XTensor * para = (XTensor*)ws.Get(i);
        XNoder::MakeGrad(para);

        if(useAdam){
            XTensor * m = new XTensor(para);
            XTensor * m2 = new XTensor(para);
            m->SetZeroAll();
            m2->SetZeroAll();
            moments.Add(m);
            moments2nd.Add(m2);
        }
    }

    adamBeta1T = 1.0F;
    adamBeta2T = 1.0F;
}

/* 
do padding on the output 
>> output - output tensor of the network
585
>> gold - gold standard
586
>> padding - padding of a batch of sentences
587
>> lsP - smoothing factor
588
*/
589
void T2TTrainer::PadOutput(XTensor * output, XTensor * gold, XTensor * padding)
590 591 592 593 594 595 596 597 598 599 600 601 602 603
{
    if(output == NULL || padding == NULL)
        return;
    
    int on = output->order;
    int * dimso = new int[on];

    memcpy(dimso, output->dimSize, sizeof(int) * on);

    output->Reshape(output->unitNum/dimso[output->order - 1], dimso[output->order - 1]);

    XTensor * padding2 = NewTensorBuf(1, &padding->unitNum, X_FLOAT, 1.0F, padding->devID, padding->mem);

    _CopyValues(padding, padding2);
604
    _MultiplyDim(output, padding2, output, 0);
605 606
    _ScaleAndShiftMe(padding2, 1e9F, -1e9F);
    _SumDim(output, padding2, output, 0);
607
    
608
    output->Reshape(on, dimso);
609 610 611 612 613 614 615
    
    if(gold != NULL){
        gold->Reshape(gold->unitNum/dimso[gold->order - 1], dimso[gold->order - 1]);
        _CopyValues(padding, padding2);
        _MultiplyDim(gold, padding2, gold, 0);
        gold->Reshape(on, dimso);
    }
616 617 618 619 620

    delete[] dimso;
    DelTensorBuf(padding2);
}

621 622 623 624 625 626 627 628 629 630
/*
recale the output and gold tensors for normalized loss
>> output - output tensor of the network
>> gold - gold standard
>> padding - padding of a batch of sentences
*/
void T2TTrainer::RescaleOutput(XTensor * output, XTensor * gold, XTensor * padding)
{
    CheckNTErrors(output->order == 3, "Wrong dimension number!");
    CheckNTErrors(gold->order == 3, "Wrong dimension number!");
631 632

    DTYPE count = _ReduceSumAll(padding);
633 634
    
    _ExpMe(output);
635
    _ScaleAndShiftMe(output, 1/count);
636
    _LogMe(output);
xuchen committed
637

638
    _ScaleAndShiftMe(gold, 1/count);
639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
}
    
/*
perform label smoothing
>> gold - gold standard
>> smoothed - result of label smoothing
>> p - smoothing factor
*/
void T2TTrainer::LabelSmooth(XTensor * gold, XTensor * smoothed, DTYPE p)
{
    CheckNTErrors(p >= 0 && p <= 1.0F, "Smoothing factor must be in range [0,1]");
    
    int n = gold->GetDim(-1);
    DTYPE q = 1.0F - p;
    DTYPE gift = p / n;
    
    InitTensor(smoothed, gold);
    _CopyValues(gold, smoothed);
    
    if(p == 0)
        return;

    _ScaleAndShiftMe(smoothed, q, gift);
}

xiaotong committed
664
}