function [trained_ensemble,results] = ensemble_training(Xc,Xs,settings) % ------------------------------------------------------------------------- % Ensemble Classification | June 2013 | version 2.0 % ------------------------------------------------------------------------- % The purpose of version 2.0 is to simplify everything as much as possible. % Here is a list of the main modifications compared to the first version of % the ensemble classifier: % - Instead of a single routine, we separated training form testing. This % allows for more flexibility in the usage. % - Training outputs the data structure 'trained_ensemble' which allows % for easy storing of the trained classifier. % - Ensemble now doesn't accept paths to features any more. Instead, it % requires the features directly (Xc - cover features, Xs - stego % features). Xc and Xs must have the same dimension and must contain % synchronized cover/stego pairs - see the attached tutorial for more % details on this. % - There is no output into a log file. So there is no hard-drive access % at all now. % - Since the training and testing routines were separated, our ensemble % implementation no longer takes care of training/testing divisions. % This is the responsibility of the user now. Again, see the attached % tutorial for examples. % - Bagging is now always on % - We fixed the fclose bug (Error: too many files open) % - Covariance caching option was removed % - Added settings.verbose = 2 option (screen output of only the last row) % - Ensemble now works even if full dimension is equal to 1 or 2. If equal % to 1, multiple decisions are still combined as different base learners % are trained on different bootstrap samples (bagging). % ------------------------------------------------------------------------- % Copyright (c) 2013 DDE Lab, Binghamton University, NY. % All Rights Reserved. % ------------------------------------------------------------------------- % Permission to use, copy, modify, and distribute this software for % educational, research and non-profit purposes, without fee, and without a % written agreement is hereby granted, provided that this copyright notice % appears in all copies. The program is supplied "as is," without any % accompanying services from DDE Lab. DDE Lab does not warrant the % operation of the program will be uninterrupted or error-free. The % end-user understands that the program was developed for research purposes % and is advised not to rely exclusively on the program for any reason. In % no event shall Binghamton University or DDE Lab be liable to any party % for direct, indirect, special, incidental, or consequential damages, % including lost profits, arising out of the use of this software. DDE Lab % disclaims any warranties, and has no obligations to provide maintenance, % support, updates, enhancements or modifications. % ------------------------------------------------------------------------- % Contact: jan@kodovsky.com | fridrich@binghamton.edu | June 2013 % http://dde.binghamton.edu/download/ensemble % ------------------------------------------------------------------------- % References: % [1] - J. Kodovsky, J. Fridrich, and V. Holub. Ensemble classifiers for % steganalysis of digital media. IEEE Transactions on Information Forensics % and Security. Currently under review. % ------------------------------------------------------------------------- % INPUT: % Xc - cover features in a row-by-row manner % Xs - corresponding stego features (needs to be synchronized!) % settings % .seed_subspaces (default = random) - PRNG seed for random subspace % generation % .seed_bootstrap (default = random) - PRNG seed for bootstrap samples % generation % .d_sub (default = 'automatic') - random subspace dimensionality; either % an integer (e.g. 200) or the string 'automatic' is accepted; in % the latter case, an automatic search for the optimal subspace % dimensionality is performed, see [1] for more details % .L (default = 'automatic') - number of random subspaces / base % learners; either an integer (e.g. 50) or the string 'automatic' % is accepted; in the latter case, an automatic stopping criterion % is used, see [1] for more details % .verbose (default = 1) - turn on/off screen output % = 0 ... no screen output % = 1 ... full screen output % = 2 ... screen output of only the last row (results) % % Parameters for the search for d_sub (when .d_sub = 'automatic'): % % .k_step (default = 200) - initial step for d_sub when searching from % left (stage 1 of Algorithm 2 in [1]) % .Eoob_tolerance (default = 0.02) - the relative tolerance for the % minimality of OOB within the search, i.e. specifies the stopping % criterion for the stage 2 in Algorithm 2 % % Both default parameters work well for most of the steganalysis scenarios. % % Parameters for automatic stopping criterion for L (when .L ='automatic'); % see [1] for more details: % % .L_kernel (default = ones(1,5)/5) - over how many values of OOB % estimates is the moving average taken over % .L_min_length (default = 25) - the minimum number of random subspaces % that will be generated % .L_memory (default = 50) - how many last OOB estimates need to stay in % the epsilon tube % .L_epsilon (default = 0.005) - specification of the epsilon tube % % According to our experiments, these values are sufficient for most of the % steganalysis tasks (different algorithms and features). Nevertheless, any % of these parameters can be modified before calling the ensemble if % desired. % ------------------------------------------------------------------------- % OUTPUT: % trained_ensemble - cell array of individual FLD base learners, each % containing the following three fields: % - subspace - random subspace indices % - w - vector of weights (normal vector to the decision boundary) % - b - bias % results - data structure with additional results of the training % procedure (training time, progress of the OOB error estimate, % summary of the search for d_sub, etc. See the attached tutorial % where we use some of these pieces of information for demonstrative % purposes % ------------------------------------------------------------------------- if ~exist('settings','var'), settings.all_default = 1; end % check settings, set default values, initial screen print [Xc,Xs,settings] = check_initial_setup(Xc,Xs,settings); % initialization of the search for d_sub [SEARCH,settings,search_counter,MIN_OOB,OOB.error] = initialize_search(settings); % search loop (if search for d_sub is to be executed) while SEARCH.in_progress search_counter = search_counter+1; % initialization [SEARCH.start_time_current_d_sub,i,next_random_subspace,TXT,base_learner] = deal(tic,0,1,'',cell(settings.max_number_base_learners,1)); % loop over individual base learners while next_random_subspace i = i+1; %%% RANDOM SUBSPACE GENERATION base_learner{i}.subspace = generate_random_subspace(settings.randstream.subspaces,settings.max_dim,settings.d_sub); %%% BOOTSTRAP INITIALIZATION OOB = bootstrap_initialization(Xc,Xs,OOB,settings); %%% TRAINING PHASE base_learner{i} = FLD_training(Xc,Xs,base_learner{i},OOB,settings); %%% OOB ERROR ESTIMATION OOB = update_oob_error_estimates(Xc,Xs,base_learner{i},OOB,i); [next_random_subspace,MSG] = getFlag_nextRandomSubspace(i,OOB,settings); % SCREEN OUTPUT CT = double(toc(SEARCH.start_time_current_d_sub)); TXT = updateTXT(TXT,sprintf(' - d_sub %s : OOB %.4f : L %i : T %.1f sec%s',k_to_string(settings.d_sub),OOB.error,i,CT,MSG),settings); end % while next_random_subspace results.search.d_sub(search_counter) = settings.d_sub; updateLog_swipe(settings,'\n'); if OOB.error terminate search SEARCH.in_progress = 0; SEARCH.optimal_d_sub = SEARCH.x(SEARCH.E==MINIMAL_ERROR); SEARCH.optimal_d_sub = SEARCH.optimal_d_sub(1); return; end if minE_id == 1 % smallest k is the best => reduce step SEARCH.step = floor(SEARCH.step/2); SEARCH = add_gridpoints(SEARCH,SEARCH.x(1)+SEARCH.step*[-1 1]); elseif minE_id == length(SEARCH.x) % largest k is the best if SEARCH.x(end) + SEARCH.step <= settings.max_dim && (min(abs(SEARCH.x(end) + SEARCH.step-SEARCH.x))>SEARCH.step/2) % continue to the right SEARCH = add_gridpoints(SEARCH,SEARCH.x(end) + SEARCH.step); else % hitting the full dimensionality if (MINIMAL_ERROR/SEARCH.E(end-1) >= 1 - settings.Eoob_tolerance) ... % desired tolerance fulfilled || SEARCH.E(end-1)-MINIMAL_ERROR < 5e-3 ... % maximal precision in terms of error set to 0.5% || SEARCH.stepSEARCH.step/2) ... % one more step to the right is not too close to any other point && ~(SEARCH.E(end)>SEARCH.E(end-1) && SEARCH.E(end)>SEARCH.E(end-2)) % the last point is not worse than the two previous ones % robustness ensurance, try one more step to the right SEARCH = add_gridpoints(SEARCH,settings.d_sub + SEARCH.step); else % best k is not at the edge of the grid (and robustness is resolved) err_around = mean(SEARCH.E(minE_id+[-1 1])); if (MINIMAL_ERROR/err_around >= 1 - settings.Eoob_tolerance) ... % desired tolerance fulfilled || err_around-MINIMAL_ERROR < 5e-3 ... % maximal precision in terms of error set to 0.5% || SEARCH.step= settings.max_dim/4, settings.d_sub_step = floor(settings.max_dim/4); end if settings.max_dim < 10, settings.d_sub_step = 1; end SEARCH.x = settings.d_sub_step*[1 2 3]; if settings.max_dim==2, SEARCH.x = [1 2]; end SEARCH.E = -ones(size(SEARCH.x)); SEARCH.terminate = 0; SEARCH.step = settings.d_sub_step; settings.d_sub = SEARCH.x(1); end search_counter = 0; MIN_OOB = 1; OOB_error = 1; function TXT = updateTXT(old,TXT,settings) if isfield(settings,'kmin') if length(TXT)>3 if ~strcmp(TXT(1:3),' - ') TXT = [' - ' TXT]; end end end if settings.verbose==1 if exist('/home','dir') % do not delete on cluster, it displays incorrectly when writing through STDOUT into file fprintf(['\n' TXT]); else fprintf([repmat('\b',1,length(old)) TXT]); end end function s = k_to_string(k) if length(k)==1 s = num2str(k); return; end s=['[' num2str(k(1))]; for i=2:length(k) s = [s ',' num2str(k(i))]; %#ok end s = [s ']']; function updateLog_swipe(settings,TXT,final) if ~exist('final','var'), final=0; end if settings.verbose==1 || (settings.verbose==2 && final==1), fprintf(TXT); end function OOB = bootstrap_initialization(Xc,Xs,OOB,settings) % initialization of the structure for OOB error estimates OOB.SUB = floor(size(Xc,1)*rand(settings.randstream.bootstrap,size(Xc,1),1))+1; OOB.ID = setdiff(1:size(Xc,1),OOB.SUB); if ~isfield(OOB,'Xc') OOB.Xc.fusion_majority_vote = zeros(size(Xc,1),1); % majority voting fusion OOB.Xc.num = zeros(size(Xc,1),1); % number of fused votes OOB.Xs.fusion_majority_vote = zeros(size(Xs,1),1); % majority voting fusion OOB.Xs.num = zeros(size(Xs,1),1); % number of fused votes end if ~isfield(OOB,'randstream_for_ties') % Doesn't really matter that we fix the seed here. This will be used % only for resolving voting ties. We are fixing this in order to make % all results nicely reproducible. OOB.randstream_for_ties = RandStream('mt19937ar','Seed',1); end function [base_learner] = findThreshold(Xm,Xp,base_learner) % find threshold through minimizing (MD+FA)/2, where MD stands for the % missed detection rate and FA for the false alarms rate P1 = Xm*base_learner.w + sqrt(1)*randn(size(Xm,1),1); P2 = Xp*base_learner.w + sqrt(1)*randn(size(Xm,1),1); L = [-ones(size(Xm,1),1);ones(size(Xp,1),1)]; [P,IX] = sort([P1;P2]); L = L(IX); Lm = (L==-1); sgn = 1; MD = 0; FA = sum(Lm); MD2=FA; FA2=MD; Emin = (FA+MD); Eact = zeros(size(L-1)); Eact2 = Eact; for idTr=1:length(P)-1 if L(idTr)==-1 FA=FA-1; MD2=MD2+1; else FA2=FA2-1; MD=MD+1; end Eact(idTr) = FA+MD; Eact2(idTr) = FA2+MD2; if Eact(idTr)0)+sum(TMP_s<0))/(length(TMP_c)+length(TMP_s)); if ~ischar(OOB) && ~isempty(OOB) H = hist([OOB.Xc.num;OOB.Xs.num],0:max([OOB.Xc.num;OOB.Xs.num])); avg_L = sum(H.*(0:length(H)-1))/sum(H); % average L in OOB OOB.x(i) = avg_L; OOB.y(i) = OOB.error; end function base_learner = FLD_training(Xc,Xs,base_learner,OOB,settings) % FLD TRAINING Xm = Xc(OOB.SUB,base_learner.subspace); Xp = Xs(OOB.SUB,base_learner.subspace); % remove constants remove = false(1,size(Xm,2)); adepts = unique([find(Xm(1,:)==Xm(2,:)) find(Xp(1,:)==Xp(2,:))]); for ad_id = adepts U1=unique(Xm(:,ad_id)); if numel(U1)==1 U2=unique(Xp(:,ad_id)); if numel(U2)==1, if U1==U2, remove(ad_id) = true; end; end end end muC = sum(Xm,1); muC = double(muC)/size(Xm,1); muS = sum(Xp,1); muS = double(muS)/size(Xp,1); mu = (muS-muC)'; % calculate sigC xc = bsxfun(@minus,Xm,muC); sigC = xc'*xc; sigC = double(sigC)/size(Xm,1); % calculate sigS xc = bsxfun(@minus,Xp,muS); sigS = xc'*xc; sigS = double(sigS)/size(Xp,1); sigCS = sigC + sigS; % regularization sigCS = sigCS + 1e-10*eye(size(sigC,1)); % check for NaN values (may occur when the feature value is constant over images) nan_values = sum(isnan(sigCS))>0; nan_values = nan_values | remove; sigCS = sigCS(~nan_values,~nan_values); mu = mu(~nan_values); lastwarn(''); warning('off','MATLAB:nearlySingularMatrix'); warning('off','MATLAB:singularMatrix'); base_learner.w = sigCS\mu; % regularization (if necessary) [txt,warnid] = lastwarn(); %#ok while strcmp(warnid,'MATLAB:singularMatrix') || (strcmp(warnid,'MATLAB:nearlySingularMatrix') && ~settings.ignore_nearly_singular_matrix_warning) lastwarn(''); if ~exist('counter','var'), counter=1; else counter = counter*5; end sigCS = sigCS + counter*eps*eye(size(sigCS,1)); base_learner.w = sigCS\mu; [txt,warnid] = lastwarn(); %#ok end warning('on','MATLAB:nearlySingularMatrix'); warning('on','MATLAB:singularMatrix'); if length(sigCS)~=length(sigC) % resolve previously found NaN values, set the corresponding elements of w equal to zero w_new = zeros(length(sigC),1); w_new(~nan_values) = base_learner.w; base_learner.w = w_new; end % find threshold to minimize FA+MD [base_learner] = findThreshold(Xm,Xp,base_learner); function results = add_search_info(results,settings,search_counter,SEARCH,i,CT) % update information about d_sub search if settings.search_for_d_sub results.search.OOB(search_counter) = SEARCH.E(SEARCH.x==results.search.d_sub(search_counter)); results.search.L(search_counter) = i; results.search.time(search_counter) = CT; end function SEARCH = add_gridpoints(SEARCH,points) % add new points for the search for d_sub for point=points if SEARCH.x(1)>point SEARCH.x = [point SEARCH.x]; SEARCH.E = [-1 SEARCH.E]; continue; end if SEARCH.x(end)