PLearn 0.1
|
00001 // -*- C++ -*- 00002 00003 // ScoreLayerVariable.cc 00004 // 00005 // Copyright (C) 2006 Olivier Delalleau 00006 // 00007 // Redistribution and use in source and binary forms, with or without 00008 // modification, are permitted provided that the following conditions are met: 00009 // 00010 // 1. Redistributions of source code must retain the above copyright 00011 // notice, this list of conditions and the following disclaimer. 00012 // 00013 // 2. Redistributions in binary form must reproduce the above copyright 00014 // notice, this list of conditions and the following disclaimer in the 00015 // documentation and/or other materials provided with the distribution. 00016 // 00017 // 3. The name of the authors may not be used to endorse or promote 00018 // products derived from this software without specific prior written 00019 // permission. 00020 // 00021 // THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 00022 // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 00023 // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 00024 // NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 00025 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 00026 // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 00027 // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 00028 // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 00029 // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 00030 // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 00031 // 00032 // This file is part of the PLearn library. For more information on the PLearn 00033 // library, go to the PLearn Web site at www.plearn.org 00034 00035 // Authors: Olivier Delalleau 00036 00040 #include "ScoreLayerVariable.h" 00041 #include "MoleculeTemplate.h" 00042 #include <plearn/var/ColumnSumVariable.h> 00043 #include <plearn/var/ConcatRowsVariable.h> 00044 #include <plearn/var/LogVariable.h> 00045 #include <plearn/var/RowSumSquareVariable.h> 00046 #include <plearn/var/SigmoidVariable.h> 00047 #include <plearn/var/SoftmaxVariable.h> 00048 #include <plearn/var/SquareRootVariable.h> 00049 #include <plearn/var/SubMatVariable.h> 00050 #include <plearn/var/SumVariable.h> 00051 #include <plearn/var/Var_operators.h> 00052 #include <plearn/var/VarRowsVariable.h> 00053 00054 namespace PLearn { 00055 using namespace std; 00056 00059 PLEARN_IMPLEMENT_OBJECT( 00060 ScoreLayerVariable, 00061 "First layer (alignment scores) of a SurfaceTemplateLearner.", 00062 "In addition to the alignment scores, this variable also replicates\n" 00063 "additional input features, if provided in the source input variable\n" 00064 "(as indicated by the 'templates_source' VMat learnt option).\n" 00065 ); 00066 00068 // ScoreLayerVariable // 00070 ScoreLayerVariable::ScoreLayerVariable(): 00071 n_active_templates(-1), 00072 n_inactive_templates(-1), 00073 normalize_by_n_features(false), 00074 seed_(-1), 00075 simple_mixture(false), 00076 random_gen(new PRandom()) 00077 {} 00078 00080 // declareOptions // 00082 void ScoreLayerVariable::declareOptions(OptionList& ol) 00083 { 00084 declareOption(ol, "icp_aligner_template", 00085 &ScoreLayerVariable::icp_aligner_template, 00086 OptionBase::buildoption, 00087 "The model of ICP aligner we want to use (will be replicated for\n" 00088 "each underlying score variable)."); 00089 00090 declareOption(ol, "n_active_templates", 00091 &ScoreLayerVariable::n_active_templates, 00092 OptionBase::buildoption, 00093 "Number of randomly chosen templates of active molecules (-1 means\n" 00094 "we take all of them and they are not shuffled)."); 00095 00096 declareOption(ol, "n_inactive_templates", 00097 &ScoreLayerVariable::n_inactive_templates, 00098 OptionBase::buildoption, 00099 "Number of randomly chosen templates of inactive molecules (-1 means\n" 00100 "we take all of them and they are not shuffled)."); 00101 00102 declareOption(ol, "normalize_by_n_features", 00103 &ScoreLayerVariable::normalize_by_n_features, 00104 OptionBase::buildoption, 00105 "If true, the score will be normalized by the number of features,\n" 00106 "i.e. scaled by one over (3 + number of common chemical features)."); 00107 00108 declareOption(ol, "seed", &ScoreLayerVariable::seed_, 00109 OptionBase::buildoption, 00110 "Seed of the random number generator (similar to a PLearner's seed)."); 00111 00112 declareOption(ol, "templates_source", 00113 &ScoreLayerVariable::templates_source, 00114 OptionBase::learntoption, // To simplify help. 00115 "The VMat templates are obtained from. This VMat's first column must\n" 00116 "be the name of a molecule, there may be other input features, and\n" 00117 "there must be a binary target indicating whether a molecule is\n" 00118 "active or inactive. This VMat is also used to initialize standard\n" 00119 "deviations of chemical features."); 00120 00121 // It is a learnt option as usually it will be set by the 00122 // SurfaceTemplateLearner above. 00123 declareOption(ol, "simple_mixture", &ScoreLayerVariable::simple_mixture, 00124 OptionBase::learntoption, 00125 "If true, then instead of being a layer of scores, it will be a\n" 00126 "single-unit layer where scores have been processed to compute an\n" 00127 "activity probability, by adding a log coefficient to each template,\n" 00128 "performing a softmax on the resulting scores and summing over the\n" 00129 "weights obtained for active templates."); 00130 00131 // Now call the parent class' declareOptions 00132 inherited::declareOptions(ol); 00133 00134 // Redeclare some options as learnt options to make help simpler. 00135 redeclareOption(ol, "varray", &ScoreLayerVariable::varray, 00136 OptionBase::learntoption, 00137 "Now a learnt option to simplify help."); 00138 00139 redeclareOption(ol, "varname", &ScoreLayerVariable::varname, 00140 OptionBase::learntoption, 00141 "Now a learnt option to simplify help."); 00142 } 00143 00145 // bprop // 00147 void ScoreLayerVariable::bprop() 00148 { 00149 // The gradient is back-propagated only on the score variables, since the 00150 // other variables are inputs that do not need be updated. 00151 int n = simple_mixture ? 1 00152 : getNActiveTemplates() + getNInactiveTemplates(); 00153 PLASSERT( n <= length() ); 00154 real* copy_grad_ptr = final_output->gradientdata; 00155 real* grad_ptr = gradientdata; 00156 for (int i = 0; i < n; i++, copy_grad_ptr++, grad_ptr++) 00157 *copy_grad_ptr += *grad_ptr; 00158 } 00159 00161 // build // 00163 void ScoreLayerVariable::build() 00164 { 00165 inherited::build(); 00166 build_(); 00167 } 00168 00170 // build_ // 00172 void ScoreLayerVariable::build_() 00173 { 00174 if (seed_ != 0) 00175 random_gen->manual_seed(seed_); 00176 00177 if (!templates_source) 00178 return; 00179 00180 // Verify that we have been given the input variable, and resize the input 00181 // array that is going to be constructed. 00182 if (varray.length() < 1) 00183 return; 00184 varray.resize(1); 00185 00186 // Obtain mappings from the templates VMat in order to be able to load the 00187 // molecule templates. 00188 VMat mappings_source_backup = mappings_source; 00189 setMappingsSource(templates_source); 00190 PLASSERT( templates_source->targetsize() == 1 ); 00191 00192 // Randomly select active and inactive templates. 00193 TVec<int> list_of_active, list_of_inactive; 00194 int n = templates_source->length(); 00195 Vec input, target; 00196 real weight; 00197 map<string, StatsCollector> chemical_stats; 00198 for (int i = 0; i < n; i++) { 00199 templates_source->getExample(i, input, target, weight); 00200 PLASSERT( fast_exact_is_equal(target[0], 0) || 00201 fast_exact_is_equal(target[0], 1) ); 00202 if (fast_exact_is_equal(target[0], 0)) 00203 list_of_inactive.append(i); 00204 else 00205 list_of_active.append(i); 00206 // Compute statistics on chemical features. 00207 PP<MoleculeTemplate> mol_template = 00208 getMoleculeTemplate(input[0], target[0]); 00209 Mat& features = mol_template->features; 00210 for (int j = 0; j < mol_template->feature_names.length(); j++) { 00211 string feature_name = mol_template->feature_names[j]; 00212 StatsCollector& stats_col = chemical_stats[feature_name]; 00213 for (int k = 0; k < features.length(); k++) 00214 stats_col.update(features(k, j)); 00215 } 00216 } 00217 n_active_in_source = list_of_active.length(); 00218 n_inactive_in_source = list_of_inactive.length(); 00219 if (n_active_templates != -1) 00220 random_gen->shuffleElements(list_of_active); 00221 if (n_inactive_templates != -1) 00222 random_gen->shuffleElements(list_of_inactive); 00223 PLASSERT( list_of_active.length() >= getNActiveTemplates() ); 00224 PLASSERT( list_of_inactive.length() >= getNInactiveTemplates() ); 00225 list_of_active.resize(getNActiveTemplates()); 00226 list_of_inactive.resize(getNInactiveTemplates()); 00227 // We do a copy of the list of active molecules instead of just appending 00228 // the inactive ones because this list may be reused later (in the case of 00229 // the simple mixture for instance). 00230 TVec<int> templates = list_of_active.copy(); 00231 templates.append(list_of_inactive); 00232 00233 // Create the Var that will run all ICPs. 00234 run_icp_var = new RunICPVariable(varray[0]); 00235 run_icp_var->setScoreLayer(this); 00236 00237 // This VarArray will list additional parameters that must be optimized. 00238 VarArray optimized_params; 00239 00240 // Create the corresponding score variables. 00241 outputs.resize(0); 00242 scaling_coeffs.resize(0); 00243 int index_in_run_icp_var = 0; // Current index. 00244 TVec<int> indices_of_active; // List of active templates. 00245 for (int i = 0; i < templates.length(); i++) { 00246 templates_source->getExample(templates[i], input, target, weight); 00247 if (is_equal(target[0], 1)) 00248 indices_of_active.append(i); 00249 PP<MoleculeTemplate> mol_template = 00250 getMoleculeTemplate(input[0], target[0]); 00251 Var molecule_coordinates(mol_template->n_points(), 3); 00252 // Create the ICP aligner that will be used for this template. 00253 CopiesMap copies; 00254 PP<ChemicalICP> icp_aligner = icp_aligner_template->deepCopy(copies); 00255 icp_aligner->mol_template = mol_template; 00256 icp_aligner->build(); 00257 // Set the current molecule of the ICP aligner to be the same template: 00258 // this will properly resize some important Vars. 00259 icp_aligner->setMolecule((MoleculeTemplate*) mol_template); 00260 // Initialize standard deviations of chemical features from the global 00261 // standard deviations. 00262 for (int j = 0; j < mol_template->feature_names.length(); j++) { 00263 string feature_name = mol_template->feature_names[j]; 00264 icp_aligner->all_template_feat_dev->matValue.column(j).fill( 00265 chemical_stats[feature_name].stddev()); 00266 } 00267 // BC TODO ADD pout des stats 00268 // Declare this new template (with associated molecule coordinates) to 00269 // the RunICPVariable. 00270 run_icp_var->addTemplate(icp_aligner, (MoleculeTemplate*) mol_template, 00271 molecule_coordinates); 00272 // Declare the RunICPVariable as parent of the feature indices, in 00273 // order to ensure these variables are used after ICP has been run. 00274 // Similarly, the variable containing the indices of the matching 00275 // neighbors has to have the RunICPVariable as parent. 00276 PP<UnaryVariable> mol_feat_indices = 00277 (UnaryVariable*) ((Variable*) icp_aligner->mol_feat_indices); 00278 mol_feat_indices->setInput((RunICPVariable*) run_icp_var); 00279 PP<UnaryVariable> template_feat_indices = 00280 (UnaryVariable*) ((Variable*) icp_aligner->template_feat_indices); 00281 template_feat_indices->setInput((RunICPVariable*) run_icp_var); 00282 PP<UnaryVariable> matching_neighbors = 00283 (UnaryVariable*) ((Variable*) icp_aligner->matching_neighbors); 00284 matching_neighbors->setInput((RunICPVariable*) run_icp_var); 00285 // Build graph of Variables. 00286 // (1) Compute the distance in chemical features. 00287 Var template_features = icp_aligner->used_template_features; 00288 icp_aligner->all_template_features->setName( 00289 "all_template_features_" + tostring(i)); 00290 optimized_params.append(icp_aligner->all_template_features); 00291 Var molecule_features_all_points = icp_aligner->used_mol_features; 00292 Var molecule_features = 00293 new VarRowsVariable(molecule_features_all_points, 00294 matching_neighbors); 00295 Var diff_features = template_features - molecule_features; 00296 Var template_features_stddev = icp_aligner->used_template_feat_dev; 00297 icp_aligner->all_template_feat_dev->setName( 00298 "all_template_feat_dev_" + tostring(i)); 00299 optimized_params.append(icp_aligner->all_template_feat_dev); 00300 Var feature_distance_at_each_point = 00301 rowSumSquare(diff_features / template_features_stddev); 00302 feature_distance_at_each_point->setName( 00303 "feature_distance_at_each_point_" + tostring(i)); 00304 Var total_feature_distance = columnSum(feature_distance_at_each_point); 00305 total_feature_distance->setName("total_feature_distance_"+tostring(i)); 00306 // (2) Compute the associated weights for the geometric distance. 00307 string wm = icp_aligner->weighting_method; 00308 Var weights; 00309 if (wm == "none") { 00310 weights = Var(mol_template->n_points(), 1); 00311 weights->value.fill(1); 00312 } else if (wm == "features_sigmoid") { 00313 Var shift = icp_aligner->weighting_params[0]; 00314 Var slope = icp_aligner->weighting_params[1]; 00315 // We add a small value to avoid zero distances, as the square root 00316 // is not differentiable at zero. 00317 Var regularized_distances = feature_distance_at_each_point + 1e-10; 00318 Var l1_dist = squareroot(regularized_distances); 00319 weights = - sigmoid(slope * (l1_dist - shift)); 00320 shift->setName("shift_" + tostring(i)); 00321 slope->setName("slope_" + tostring(i)); 00322 weights->setName("weights_" + tostring(i)); 00323 optimized_params.append(shift); 00324 optimized_params.append(slope); 00325 } else { 00326 PLERROR("In ScoreLayerVariable::build_ - Unsupported value for " 00327 "'weighting_method'"); 00328 } 00329 // (3) Compute the distance in geometric coordinates. 00330 Var template_coordinates = 00331 PLearn::subMat((RunICPVariable*) run_icp_var, 00332 index_in_run_icp_var, 0, 00333 mol_template->n_points(), 3); 00334 index_in_run_icp_var += mol_template->n_points(); 00335 Var diff_coordinates = template_coordinates - molecule_coordinates; 00336 Var template_coordinates_stddev = icp_aligner->template_geom_dev; 00337 template_coordinates_stddev->setName( 00338 "template_coordinates_stddev_" + tostring(i)); 00339 optimized_params.append(template_coordinates_stddev); 00340 Var distance_at_each_point = 00341 rowSumSquare(diff_coordinates / template_coordinates_stddev); 00342 distance_at_each_point->setName("distance_at_each_point_"+tostring(i)); 00343 Var weighted_total_geometric_distance = 00344 columnSum(distance_at_each_point * weights); 00345 weighted_total_geometric_distance->setName( 00346 "weighted_total_geometric_distance_" + tostring(i)); 00347 // (4) Sum to obtain the final score. 00348 Var total_cost = 00349 -0.5 * (total_feature_distance + 00350 weighted_total_geometric_distance) 00351 - sum(log(template_coordinates_stddev)) 00352 - sum(log(template_features_stddev)); 00353 // Note that at build time, the scaling coefficient is always equal to 00354 // one over the number of points, regardless of the value of option 00355 // 'normalize_by_n_features': this is because the number of features 00356 // depends on the molecule being aligned, thus the scaling coefficient 00357 // will be modified at run time by the RunICPVariable. 00358 Var scaling_var = var(real(1.0 / mol_template->n_points())); 00359 scaling_coeffs.append(scaling_var); 00360 total_cost = scaling_var * total_cost; 00361 00362 // (5) Compute path on which sizes will have to be updated after ICP. 00363 VarArray path_inputs = icp_aligner->used_template_features & 00364 icp_aligner->used_mol_features & 00365 icp_aligner->used_template_feat_dev; 00366 VarArray path_outputs = total_cost; 00367 VarArray& path_to_resize = run_icp_var->getPathsToResize(i); 00368 path_to_resize = propagationPath(path_inputs, path_outputs); 00369 00370 // (6) Add template coefficient in the case of the simple mixture mode. 00371 if (simple_mixture) { 00372 Var coeff = var(real(1.0 / templates.length())); 00373 coeff->min_value = 1e-10; 00374 coeff->max_value = 1; 00375 total_cost += log(coeff); 00376 optimized_params.append(coeff); 00377 } 00378 00379 // (7) Build output array. 00380 outputs.append(total_cost); 00381 } 00382 00383 // Append the additional input features if they are present. 00384 if (templates_source->inputsize() > 1) { 00385 if (simple_mixture) 00386 PLERROR("In ScoreLayerVariable::build_ - The simple mixture model " 00387 "is not meant to be used with extra input information"); 00388 Var input_var = varray[0]; 00389 PLASSERT( input_var->width() == 1 ); 00390 Var input_minus_molecule_id = 00391 PLearn::subMat(input_var, 1, 0, input_var->length() - 1, 1); 00392 outputs.append(input_minus_molecule_id); 00393 } 00394 00395 // Concatenate all outputs in a single Variable. 00396 final_output = vconcat(outputs); 00397 00398 // Build softmax output for activity probability in the case of the simple 00399 // mixture model. 00400 if (simple_mixture) { 00401 Var softmax_output = softmax(final_output); 00402 PLASSERT( list_of_active.length() > 0 ); 00403 PLASSERT( softmax_output->width() == 1 ); 00404 // Select only active templates (we assume - and verify - here that 00405 // they they are the first ones). 00406 if (min(list_of_active) != 0 || 00407 max(list_of_active) != list_of_active.length() - 1) 00408 { 00409 PLERROR("In ScoreLayerVariable::build_ - The active templates must" 00410 " be first"); 00411 } 00412 final_output = PLearn::subMat(softmax_output, 0, 0, 00413 list_of_active.length(), 1); 00414 // Then we just sum over the resulting posteriors. 00415 final_output = sum(final_output); 00416 } 00417 00418 // The final 'varray' will contain, in this order: 00419 // - the input variable to this layer (already here) 00420 // - the parameters (means, standard deviations, ...) 00421 // - the final output 00422 // The final output is not a parameter that will be updated during 00423 // initialization, but it needs to be in this Variable's parents so that 00424 // back-propagation is correctly performed. 00425 varray.append(optimized_params); 00426 varray.append(final_output); 00427 // We have changed a parent's option, we should re-build. 00428 inherited::build(); 00429 00430 // Restore the mappings source that was given prior to calling build(). 00431 if (mappings_source_backup) 00432 setMappingsSource(mappings_source_backup); 00433 } 00434 00436 // fprop // 00438 void ScoreLayerVariable::fprop() 00439 { 00440 // Just need to copy the data from 'final_output'. 00441 int n = nelems(); 00442 PLASSERT( n == length() ); 00443 real* copy_ptr = final_output->valuedata; 00444 real* ptr = valuedata; 00445 for (int i = 0; i < n; i++, ptr++, copy_ptr++) 00446 *ptr = *copy_ptr; 00447 } 00448 00450 // getMolecule // 00452 PP<Molecule> ScoreLayerVariable::getMolecule(real molecule_id) 00453 { 00454 PLASSERT( mappings_source ); 00455 PPath molecule_path = mappings_source->getValString(0, molecule_id); 00456 if (molecule_path.isEmpty()) 00457 PLERROR("In ScoreLayerVariable::getMolecule - Could not find " 00458 "associated mapping"); 00459 string canonic_path = molecule_path.canonical(); 00460 // Load the molecule if necessary. 00461 Molecule* molecule = 0; 00462 if (molecules.find(canonic_path) == molecules.end()) { 00463 molecule = 00464 new Molecule(molecule_path); 00465 molecules[canonic_path] = molecule; 00466 } else 00467 molecule = molecules[canonic_path]; 00468 PLASSERT( molecule ); 00469 return molecule; 00470 } 00471 00473 // getMoleculeTemplate // 00475 PP<MoleculeTemplate> ScoreLayerVariable::getMoleculeTemplate(real molecule_id, 00476 real activity) 00477 { 00478 PLASSERT( fast_exact_is_equal(activity, 0) || 00479 fast_exact_is_equal(activity, 1) || 00480 fast_exact_is_equal(activity, -1) ); 00481 PLASSERT( mappings_source ); 00482 PPath molecule_path = mappings_source->getValString(0, molecule_id); 00483 if (molecule_path.isEmpty()) 00484 PLERROR("In ScoreLayerVariable::getMolecule - Could not find " 00485 "associated mapping"); 00486 string canonic_path = molecule_path.canonical(); 00487 // Load the molecule if necessary. 00488 MoleculeTemplate* molecule = 0; 00489 if (molecule_templates.find(canonic_path) == molecule_templates.end()) { 00490 molecule = 00491 new MoleculeTemplate(molecule_path, int(activity)); 00492 molecule_templates[canonic_path] = molecule; 00493 } else 00494 molecule = molecule_templates[canonic_path]; 00495 PLASSERT( molecule ); 00496 return molecule; 00497 } 00498 00500 // getNActiveTemplates // 00502 int ScoreLayerVariable::getNActiveTemplates() 00503 { 00504 if (n_active_templates >= 0) 00505 return n_active_templates; 00506 else 00507 return n_active_in_source; 00508 } 00509 00511 // getNInactiveTemplates // 00513 int ScoreLayerVariable::getNInactiveTemplates() 00514 { 00515 if (n_inactive_templates >= 0) 00516 return n_inactive_templates; 00517 else 00518 return n_inactive_in_source; 00519 } 00520 00522 // makeDeepCopyFromShallowCopy // 00524 void ScoreLayerVariable::makeDeepCopyFromShallowCopy(CopiesMap& copies) 00525 { 00526 inherited::makeDeepCopyFromShallowCopy(copies); 00527 00528 deepCopyField(run_icp_var, copies); 00529 deepCopyField(icp_aligner_template, copies); 00530 deepCopyField(templates_source, copies); 00531 deepCopyField(mappings_source, copies); 00532 deepCopyField(random_gen, copies); 00533 deepCopyField(outputs, copies); 00534 varDeepCopyField(final_output, copies); 00535 deepCopyField(molecule_templates, copies); 00536 deepCopyField(molecules, copies); 00537 deepCopyField(scaling_coeffs, copies); 00538 } 00539 00541 // recomputeSize // 00543 void ScoreLayerVariable::recomputeSize(int& l, int& w) const 00544 { 00545 if (final_output) { 00546 l = final_output->length(); 00547 w = final_output->width(); 00548 } else { 00549 l = w = 0; 00550 } 00551 } 00552 00554 // setMappingsSource // 00556 void ScoreLayerVariable::setMappingsSource(const VMat& source_vmat) 00557 { 00558 mappings_source = source_vmat; 00559 } 00560 00562 // setScalingCoefficient // 00564 void ScoreLayerVariable::setScalingCoefficient(int i, real coeff) 00565 { 00566 PLASSERT( i << scaling_coeffs.length() ); 00567 PLASSERT( scaling_coeffs[i]->nelems() == 1 ); 00568 scaling_coeffs[i]->value[0] = coeff; 00569 } 00570 00571 } // end of namespace PLearn 00572 00573 00574 /* 00575 Local Variables: 00576 mode:c++ 00577 c-basic-offset:4 00578 c-file-style:"stroustrup" 00579 c-file-offsets:((innamespace . 0)(inline-open . 0)) 00580 indent-tabs-mode:nil 00581 fill-column:79 00582 End: 00583 */ 00584 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=79 :